Skip to content
This repository has been archived by the owner on May 18, 2021. It is now read-only.

Commit

Permalink
feat: One keyring item for session (#174)
Browse files Browse the repository at this point in the history
* Add some notes and docstrings

* Move original KeyringSessions to its own internal package

* Avoid passing all profiles to sessioncache

* remove sessioncache Delete

* Refactor session cache into Key and Store parts

* Rename Retrieve -> Get and Store -> Put

* Flatten sessioncache package heirarchy

* Add cache hit/miss/expired/error debug messages

* add sessionCacheInterface

* Add SingleKrItemStore

* fix debug fmt

* Add --session-cache-single-item

* Fixes; remove versioned db

We can cross that bridge when we come to it

* docs

* f: remove sequence number

* Clean up; add missing error handling

* Add sessioncache store tests

Introduces golang.org/x/xerrors, until we can start to use go 1.13
errors reliably.

* Remove dependency on github.com/pkg/errors

Just use stdlib errors + xerrors
  • Loading branch information
nickatsegment authored Jul 9, 2019
1 parent 11c2803 commit 6658fa0
Show file tree
Hide file tree
Showing 16 changed files with 496 additions and 145 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ For Linux / Ubuntu add the following to your bash config / zshrc etc:
export AWS_OKTA_BACKEND=secret-service
```

## --session-cache-single-item aka AWS_OKTA_SESSION_CACHE_SINGLE_ITEM (alpha)

This flag enables a new secure session cache that stores all sessions in the same keyring item. For macOS users, this means drastically fewer authorization prompts when upgrading or running local builds.

No provision is made to migrate sessions between session caches.

Implemented in [https://github.com/segmentio/aws-okta/issues/146](#146).

## Local Development

If you're developing in Linux, you'll need to get `libusb`. For Ubuntu, install the libusb-1.0-0-dev or use the `Dockerfile` provided in the repo.
Expand Down
2 changes: 2 additions & 0 deletions cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ func envRun(cmd *cobra.Command, args []string) error {
})
}

opts.SessionCacheSingleItem = flagSessionCacheSingleItem

p, err := lib.NewProvider(kr, profile, opts)
if err != nil {
return err
Expand Down
12 changes: 7 additions & 5 deletions cmd/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ var (
)

func mustListProfiles() lib.Profiles {
profiles, err := listProfiles()
if err != nil {
log.Panicf("Failed to list profiles: %v", err)
}
return profiles
profiles, err := listProfiles()
if err != nil {
log.Panicf("Failed to list profiles: %v", err)
}
return profiles
}

// execCmd represents the exec command
Expand Down Expand Up @@ -166,6 +166,8 @@ func execRun(cmd *cobra.Command, args []string) error {
})
}

opts.SessionCacheSingleItem = flagSessionCacheSingleItem

p, err := lib.NewProvider(kr, profile, opts)
if err != nil {
return err
Expand Down
10 changes: 6 additions & 4 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import (

// loginCmd represents the login command
var loginCmd = &cobra.Command{
Use: "login <profile>",
Short: "login will authenticate you through okta and allow you to access your AWS environment through a browser",
RunE: loginRun,
PreRun: loginPre,
Use: "login <profile>",
Short: "login will authenticate you through okta and allow you to access your AWS environment through a browser",
RunE: loginRun,
PreRun: loginPre,
ValidArgs: listProfileNames(mustListProfiles()),
}

Expand Down Expand Up @@ -107,6 +107,8 @@ func loginRun(cmd *cobra.Command, args []string) error {
})
}

opts.SessionCacheSingleItem = flagSessionCacheSingleItem

p, err := lib.NewProvider(kr, profile, opts)
if err != nil {
return err
Expand Down
41 changes: 30 additions & 11 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package cmd

import (
"errors"
"fmt"
"os"
"strconv"

"errors"

"github.com/99designs/keyring"
analytics "github.com/segmentio/analytics-go"
"github.com/segmentio/aws-okta/lib"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
)

// Errors returned from frontend commands
Expand All @@ -28,23 +31,26 @@ const (

// global flags
var (
backend string
mfaConfig lib.MFAConfig
debug bool
version string
analyticsWriteKey string
analyticsEnabled bool
analyticsClient analytics.Client
username string
backend string
mfaConfig lib.MFAConfig
debug bool
version string
analyticsWriteKey string
analyticsEnabled bool
analyticsClient analytics.Client
username string
flagSessionCacheSingleItem bool
)

const envSessionCacheSingleItem = "AWS_OKTA_SESSION_CACHE_SINGLE_ITEM"

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "aws-okta",
Short: "aws-okta allows you to authenticate with AWS using your okta credentials",
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRun: prerun,
PersistentPreRunE: prerunE,
PersistentPostRun: postrun,
}

Expand All @@ -64,7 +70,7 @@ func Execute(vers string, writeKey string) {
}
}

func prerun(cmd *cobra.Command, args []string) {
func prerunE(cmd *cobra.Command, args []string) error {
// Load backend from env var if not set as a flag
if !cmd.Flags().Lookup("backend").Changed {
backendFromEnv, ok := os.LookupEnv("AWS_OKTA_BACKEND")
Expand All @@ -77,6 +83,17 @@ func prerun(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
}

if !cmd.Flags().Lookup("session-cache-single-item").Changed {
val, ok := os.LookupEnv(envSessionCacheSingleItem)
if ok {
valb, err := strconv.ParseBool(val)
if err != nil {
return xerrors.Errorf("couldn't parse as bool: %s: %w", val, err)
}
flagSessionCacheSingleItem = valb
}
}

if analyticsEnabled {
// set up analytics client
analyticsClient, _ = analytics.NewWithConfig(analyticsWriteKey, analytics.Config{
Expand All @@ -90,6 +107,7 @@ func prerun(cmd *cobra.Command, args []string) {
Set("aws-okta-version", version),
})
}
return nil
}

func postrun(cmd *cobra.Command, args []string) {
Expand All @@ -108,6 +126,7 @@ func init() {
RootCmd.PersistentFlags().StringVarP(&mfaConfig.DuoDevice, "mfa-duo-device", "", "phone1", "Device to use phone1, phone2, u2f or token")
RootCmd.PersistentFlags().StringVarP(&backend, "backend", "b", "", fmt.Sprintf("Secret backend to use %s", backendsAvailable))
RootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
RootCmd.PersistentFlags().BoolVarP(&flagSessionCacheSingleItem, "session-cache-single-item", "", false, fmt.Sprintf("(alpha) Enable single-item session cache; aka %s", envSessionCacheSingleItem))
}

func updateMfaConfig(cmd *cobra.Command, profiles lib.Profiles, profile string, config *lib.MFAConfig) {
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/kr/pretty v0.1.0 // indirect
github.com/marshallbrekka/go-u2fhost v0.0.0-20170128051651-72b0e7a3f583
github.com/marshallbrekka/go.hid v0.0.0-20161227002717-2c1c4616a9e7 // indirect
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747
github.com/mitchellh/go-homedir v1.0.0
github.com/segmentio/analytics-go v3.0.1+incompatible
github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c // indirect
github.com/sirupsen/logrus v1.4.1
Expand All @@ -27,12 +27,14 @@ require (
github.com/spf13/cobra v0.0.0-20170621173259-31694f19adee
github.com/spf13/pflag v1.0.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.3.0
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
github.com/vitaminwater/cgo.wchar v0.0.0-20160320123332-5dd6f4be3f2a // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20190311183353-d8887717615a
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb // indirect
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ github.com/marshallbrekka/go-u2fhost v0.0.0-20170128051651-72b0e7a3f583 h1:PmKze
github.com/marshallbrekka/go-u2fhost v0.0.0-20170128051651-72b0e7a3f583/go.mod h1:U9kRL9P37LGrkikKWuekWsReXRKe2fkZdRSXpI7pP3A=
github.com/marshallbrekka/go.hid v0.0.0-20161227002717-2c1c4616a9e7 h1:OWtSIWxw/A5amtd2wDFMtFILVoCuHC+k4V5Y/0aM4/Y=
github.com/marshallbrekka/go.hid v0.0.0-20161227002717-2c1c4616a9e7/go.mod h1:EKx8PPAql1ncHKW3HCDlw4d7ELZ/kmfiDJjLfNf+Ek0=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747 h1:eQox4Rh4ewJF+mqYPxCkmBAirRnPaHEB26UkNuPyjlk=
github.com/mitchellh/go-homedir v0.0.0-20161203194507-b8bc1bf76747/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/analytics-go v3.0.1+incompatible h1:W7T3ieNQjPFMb+SE8SAVYo6mPkKK/Y37wYdiNf5lCVg=
Expand Down Expand Up @@ -86,6 +86,8 @@ golang.org/x/sys v0.0.0-20190516110030-61b9204099cb h1:k07iPOt0d6nEnwXF+kHB+iEg+
golang.org/x/sys v0.0.0-20190516110030-61b9204099cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
Expand Down
39 changes: 39 additions & 0 deletions internal/sessioncache/key_orig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package sessioncache

import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"time"
)

type OrigKey struct {
ProfileName string
ProfileConf map[string]string
Duration time.Duration
}

// Key returns a key for the keyring item. This is a string containing the source profile name,
// the profile name, and a hash of the duration
//
// this is a copy of KeyringSessions.key and should preserve behavior, *except* that it assumes `profileName`
// is a valid and existing profile name
func (k OrigKey) Key() string {
// nick: I don't understand this at all. This key function is roughly:
// sourceProfileName + hex(md5(duration + json(profileConf)))
// - why md5?
// - why the JSON of the whole profile? (especially strange considering JSON map order is undetermined)
// TODO(nick): document this
var source string
if source = k.ProfileConf["source_profile"]; source == "" {
source = k.ProfileName
}
hasher := md5.New()
hasher.Write([]byte(k.Duration.String()))

enc := json.NewEncoder(hasher)
enc.Encode(k.ProfileConf)

return fmt.Sprintf("%s session (%x)", source, hex.EncodeToString(hasher.Sum(nil))[0:10])
}
29 changes: 29 additions & 0 deletions internal/sessioncache/sessioncache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// sessioncache caches sessions (sts.Credentials)
//
// sessioncache splits Stores (the way cache items are stored) from Keys
// (the way cache items are looked up/replaced)
package sessioncache

import (
"encoding/json"
"errors"

"github.com/aws/aws-sdk-go/service/sts"
)

// Session adds a session name to sts.Credentials
type Session struct {
Name string
sts.Credentials
}

func (s *Session) Bytes() ([]byte, error) {
return json.Marshal(s)
}

// Key is used to compute the cache key for a session
type Key interface {
Key() string
}

var ErrSessionExpired = errors.New("session expired")
73 changes: 73 additions & 0 deletions internal/sessioncache/store_kritempersession.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package sessioncache

import (
"encoding/json"
"time"

"github.com/99designs/keyring"
log "github.com/sirupsen/logrus"

// use xerrors until 1.13 is stable/oldest supported version
"golang.org/x/xerrors"
)

// KrItemPerSessionStore stores one session in one keyring item
//
// This is the classic session store implementation. Its main drawback is that on macOS,
// without code signing, you need to reauthorize the binary between upgrades *for each
// item*.
type KrItemPerSessionStore struct {
Keyring keyring.Keyring
}

// Get returns the session from the keyring at k.Key()
//
// If the keyring item is not found, returns wrapped keyring.ErrKeyNotFound
//
// If the session is found, but is expired, returns wrapped ErrSessionExpired
func (s *KrItemPerSessionStore) Get(k Key) (*Session, error) {
keyStr := k.Key()
item, err := s.Keyring.Get(keyStr)
if err != nil {
log.Debugf("cache get `%s`: miss (read error): %s", keyStr, err)
return nil, xerrors.Errorf("failed Keyring.Get(%q): %w", keyStr, err)
}

var session Session

if err = json.Unmarshal(item.Data, &session); err != nil {
log.Debugf("cache get `%s`: miss (unmarshal error): %s", keyStr, err)
return nil, xerrors.Errorf("failed unmarshal for %q: %w", keyStr, err)
}

if session.Expiration.Before(time.Now()) {
log.Debugf("cache get `%s`: expired", keyStr)
return nil, xerrors.Errorf("%q expired at %s: %w", keyStr, session.Expiration, ErrSessionExpired)
}

log.Debugf("cache get `%s`: hit", keyStr)
return &session, nil
}

func (s *KrItemPerSessionStore) Put(k Key, session *Session) error {
keyStr := k.Key()
bytes, err := session.Bytes()
if err != nil {
log.Debugf("cache put `%s`: error (marshal): %s", keyStr, err)
return err
}

log.Debugf("Writing session for %s to keyring", session.Name)
item := keyring.Item{
Key: k.Key(),
Label: "aws session for " + session.Name,
Data: bytes,
KeychainNotTrustApplication: false,
}
if err := s.Keyring.Set(item); err != nil {
log.Debugf("cache put `%s`: error (write): %s", keyStr, err)
return err
}

return nil
}
15 changes: 15 additions & 0 deletions internal/sessioncache/store_kritempersession_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package sessioncache

import (
"testing"

"github.com/99designs/keyring"
)

func TestKrItemPerSessionStore(t *testing.T) {
testStore(t, func() store {
return &KrItemPerSessionStore{
Keyring: keyring.NewArrayKeyring([]keyring.Item{}),
}
})
}
Loading

0 comments on commit 6658fa0

Please sign in to comment.