Skip to content

Commit 40ecd2a

Browse files
committed
Use Idp Account Name as key for credentials store
At the moment the credentials are stored using the idp server url as a primary key for the keychain store. This is very useful if you need to authenticate through different providers or environment (and thus different endpoints), however this prevents the use case where users may want to use different idp accounts to authenticate using different users. A typical example of this is that some users often have a normal user and an admin user with different properties, roles and limitations. This change is refactoring how credentials are stored across the code base to use the idp Name instead of the server URL.
1 parent f872b40 commit 40ecd2a

File tree

13 files changed

+129
-70
lines changed

13 files changed

+129
-70
lines changed

cmd/saml2aws/commands/configure.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"path"
77

88
"github.com/pkg/errors"
9-
"github.com/versent/saml2aws/v2"
9+
saml2aws "github.com/versent/saml2aws/v2"
1010
"github.com/versent/saml2aws/v2/helper/credentials"
1111
"github.com/versent/saml2aws/v2/pkg/cfg"
1212
"github.com/versent/saml2aws/v2/pkg/flags"
@@ -68,14 +68,14 @@ func storeCredentials(configFlags *flags.CommonFlags, account *cfg.IDPAccount) e
6868
return nil
6969
}
7070
if configFlags.Password != "" {
71-
if err := credentials.SaveCredentials(account.URL, account.Username, configFlags.Password); err != nil {
71+
if err := credentials.SaveCredentials(account.Name, account.URL, account.Username, configFlags.Password); err != nil {
7272
return errors.Wrap(err, "error storing password in keychain")
7373
}
7474
} else {
7575
password := prompter.Password("Password")
7676
if password != "" {
7777
if confirmPassword := prompter.Password("Confirm"); confirmPassword == password {
78-
if err := credentials.SaveCredentials(account.URL, account.Username, password); err != nil {
78+
if err := credentials.SaveCredentials(account.Name, account.URL, account.Username, password); err != nil {
7979
return errors.Wrap(err, "error storing password in keychain")
8080
}
8181
} else {
@@ -91,7 +91,8 @@ func storeCredentials(configFlags *flags.CommonFlags, account *cfg.IDPAccount) e
9191
log.Println("OneLogin provider requires --client_id and --client_secret flags to be set.")
9292
os.Exit(1)
9393
}
94-
if err := credentials.SaveCredentials(path.Join(account.URL, OneLoginOAuthPath), configFlags.ClientID, configFlags.ClientSecret); err != nil {
94+
// we store the OneLogin token in a different secret (idpName + the one login suffix)
95+
if err := credentials.SaveCredentials(account.Name+credentials.OneLoginTokenSuffix, path.Join(account.URL, OneLoginOAuthPath), configFlags.ClientID, configFlags.ClientSecret); err != nil {
9596
return errors.Wrap(err, "error storing client_id and client_secret in keychain")
9697
}
9798
}

cmd/saml2aws/commands/list_roles.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
"github.com/pkg/errors"
1010
"github.com/sirupsen/logrus"
11-
"github.com/versent/saml2aws/v2"
11+
saml2aws "github.com/versent/saml2aws/v2"
1212
"github.com/versent/saml2aws/v2/helper/credentials"
1313
"github.com/versent/saml2aws/v2/pkg/flags"
1414
"github.com/versent/saml2aws/v2/pkg/samlcache"
@@ -80,7 +80,7 @@ func ListRoles(loginFlags *flags.LoginExecFlags) error {
8080
}
8181

8282
if !loginFlags.CommonFlags.DisableKeychain {
83-
err = credentials.SaveCredentials(loginDetails.URL, loginDetails.Username, loginDetails.Password)
83+
err = credentials.SaveCredentials(loginDetails.IdpName, loginDetails.URL, loginDetails.Username, loginDetails.Password)
8484
if err != nil {
8585
return errors.Wrap(err, "error storing password in keychain")
8686
}

cmd/saml2aws/commands/login.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/aws/aws-sdk-go/service/sts"
1515
"github.com/pkg/errors"
1616
"github.com/sirupsen/logrus"
17-
"github.com/versent/saml2aws/v2"
17+
saml2aws "github.com/versent/saml2aws/v2"
1818
"github.com/versent/saml2aws/v2/helper/credentials"
1919
"github.com/versent/saml2aws/v2/pkg/awsconfig"
2020
"github.com/versent/saml2aws/v2/pkg/cfg"
@@ -122,7 +122,7 @@ func Login(loginFlags *flags.LoginExecFlags) error {
122122
}
123123

124124
if !loginFlags.CommonFlags.DisableKeychain {
125-
err = credentials.SaveCredentials(loginDetails.URL, loginDetails.Username, loginDetails.Password)
125+
err = credentials.SaveCredentials(loginDetails.IdpName, loginDetails.URL, loginDetails.Username, loginDetails.Password)
126126
if err != nil {
127127
return errors.Wrap(err, "Error storing password in keychain.")
128128
}
@@ -174,15 +174,20 @@ func buildIdpAccount(loginFlags *flags.LoginExecFlags) (*cfg.IDPAccount, error)
174174

175175
func resolveLoginDetails(account *cfg.IDPAccount, loginFlags *flags.LoginExecFlags) (*creds.LoginDetails, error) {
176176

177-
// log.Printf("loginFlags %+v", loginFlags)
178-
179-
loginDetails := &creds.LoginDetails{URL: account.URL, Username: account.Username, MFAToken: loginFlags.CommonFlags.MFAToken, DuoMFAOption: loginFlags.DuoMFAOption}
177+
loginDetails := &creds.LoginDetails{
178+
URL: account.URL,
179+
Username: account.Username,
180+
MFAToken: loginFlags.CommonFlags.MFAToken,
181+
DuoMFAOption: loginFlags.DuoMFAOption,
182+
IdpName: account.Name,
183+
IdpProvider: account.Provider,
184+
}
180185

181186
log.Printf("Using IdP Account %s to access %s %s", loginFlags.CommonFlags.IdpAccount, account.Provider, account.URL)
182187

183188
var err error
184189
if !loginFlags.CommonFlags.DisableKeychain {
185-
err = credentials.LookupCredentials(loginDetails, account.Provider)
190+
err = credentials.LookupCredentials(loginDetails)
186191
if err != nil {
187192
if !credentials.IsErrCredentialsNotFound(err) {
188193
return nil, errors.Wrap(err, "Error loading saved password.")

cmd/saml2aws/commands/login_test.go

+52-7
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"time"
77

88
"github.com/stretchr/testify/assert"
9-
"github.com/versent/saml2aws/v2"
9+
saml2aws "github.com/versent/saml2aws/v2"
1010
"github.com/versent/saml2aws/v2/pkg/awsconfig"
1111
"github.com/versent/saml2aws/v2/pkg/cfg"
1212
"github.com/versent/saml2aws/v2/pkg/creds"
@@ -15,10 +15,17 @@ import (
1515

1616
func TestResolveLoginDetailsWithFlags(t *testing.T) {
1717

18-
commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAToken: "123456", SkipPrompt: true}
18+
commonFlags := &flags.CommonFlags{
19+
URL: "https://id.example.com",
20+
Username: "wolfeidau",
21+
Password: "testtestlol",
22+
MFAToken: "123456",
23+
SkipPrompt: true,
24+
}
1925
loginFlags := &flags.LoginExecFlags{CommonFlags: commonFlags}
2026

2127
idpa := &cfg.IDPAccount{
28+
Name: "AccountName",
2229
URL: "https://id.example.com",
2330
MFA: "none",
2431
Provider: "Ping",
@@ -27,16 +34,31 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) {
2734
loginDetails, err := resolveLoginDetails(idpa, loginFlags)
2835

2936
assert.Empty(t, err)
30-
assert.Equal(t, &creds.LoginDetails{Username: "wolfeidau", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456"}, loginDetails)
37+
assert.Equal(t,
38+
&creds.LoginDetails{
39+
IdpName: "AccountName",
40+
IdpProvider: "Ping",
41+
Username: "wolfeidau",
42+
Password: "testtestlol",
43+
URL: "https://id.example.com",
44+
MFAToken: "123456",
45+
}, loginDetails)
3146
}
3247

3348
func TestOktaResolveLoginDetailsWithFlags(t *testing.T) {
3449

3550
// Default state - user did not supply values for DisableSessions and DisableSessions
36-
commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "testuser", Password: "testtestlol", MFAToken: "123456", SkipPrompt: true}
51+
commonFlags := &flags.CommonFlags{
52+
URL: "https://id.example.com",
53+
Username: "testuser",
54+
Password: "testtestlol",
55+
MFAToken: "123456",
56+
SkipPrompt: true,
57+
}
3758
loginFlags := &flags.LoginExecFlags{CommonFlags: commonFlags}
3859

3960
idpa := &cfg.IDPAccount{
61+
Name: "AnotherAccountName",
4062
URL: "https://id.example.com",
4163
MFA: "none",
4264
Provider: "Okta",
@@ -47,19 +69,42 @@ func TestOktaResolveLoginDetailsWithFlags(t *testing.T) {
4769
assert.Nil(t, err)
4870
assert.False(t, idpa.DisableSessions, fmt.Errorf("default state, DisableSessions should be false"))
4971
assert.False(t, idpa.DisableRememberDevice, fmt.Errorf("default state, DisableRememberDevice should be false"))
50-
assert.Equal(t, &creds.LoginDetails{Username: "testuser", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456"}, loginDetails)
72+
assert.Equal(t,
73+
&creds.LoginDetails{
74+
IdpName: "AnotherAccountName",
75+
IdpProvider: "Okta",
76+
Username: "testuser",
77+
Password: "testtestlol",
78+
URL: "https://id.example.com",
79+
MFAToken: "123456",
80+
}, loginDetails)
5181

5282
// User disabled keychain, resolveLoginDetails should set the account's DisableSessions and DisableSessions fields to true
5383

54-
commonFlags = &flags.CommonFlags{URL: "https://id.example.com", Username: "testuser", Password: "testtestlol", MFAToken: "123456", SkipPrompt: true, DisableKeychain: true}
84+
commonFlags = &flags.CommonFlags{
85+
URL: "https://id.example.com",
86+
Username: "testuser",
87+
Password: "testtestlol",
88+
MFAToken: "123456",
89+
SkipPrompt: true,
90+
DisableKeychain: true,
91+
}
5592
loginFlags = &flags.LoginExecFlags{CommonFlags: commonFlags}
5693

5794
loginDetails, err = resolveLoginDetails(idpa, loginFlags)
5895

5996
assert.Nil(t, err)
6097
assert.True(t, idpa.DisableSessions, fmt.Errorf("user disabled keychain, DisableSessions should be true"))
6198
assert.True(t, idpa.DisableRememberDevice, fmt.Errorf("user disabled keychain, DisableRememberDevice should be true"))
62-
assert.Equal(t, &creds.LoginDetails{Username: "testuser", Password: "testtestlol", URL: "https://id.example.com", MFAToken: "123456"}, loginDetails)
99+
assert.Equal(t,
100+
&creds.LoginDetails{
101+
IdpName: "AnotherAccountName",
102+
IdpProvider: "Okta",
103+
Username: "testuser",
104+
Password: "testtestlol",
105+
URL: "https://id.example.com",
106+
MFAToken: "123456",
107+
}, loginDetails)
63108

64109
}
65110

helper/credentials/credentials.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package credentials
22

33
import (
44
"errors"
5+
"fmt"
56
)
67

78
var (
@@ -14,25 +15,35 @@ var (
1415

1516
// Credentials holds the information shared between saml2aws and the credentials store.
1617
type Credentials struct {
18+
IdpName string
1719
ServerURL string
1820
Username string
1921
Secret string
2022
}
2123

22-
// CredsLabel saml2aws credentials should be labeled as such in credentials stores that allow labelling.
23-
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
24-
// Windows credentials manager and Linux libsecret. Default value is "saml2aws Credentials"
25-
var CredsLabel = "saml2aws Credentials"
24+
const (
25+
// CredsLabel saml2aws credentials should be labeled as such in credentials stores that allow labelling.
26+
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
27+
// Windows credentials manager and Linux libsecret. Default value is "saml2aws Credentials"
28+
CredsLabel = "saml2aws Credentials"
29+
CredsKeyPrefix = "saml2aws_credentials"
30+
OktaSessionCookieSuffix = "_okta_session"
31+
OneLoginTokenSuffix = "_onelogin_token"
32+
)
33+
34+
func GetKeyFromAccount(accountName string) string {
35+
return fmt.Sprintf("%s_%s", CredsKeyPrefix, accountName)
36+
}
2637

2738
// Helper is the interface a credentials store helper must implement.
2839
type Helper interface {
2940
// Add appends credentials to the store.
3041
Add(*Credentials) error
3142
// Delete removes credentials from the store.
32-
Delete(serverURL string) error
43+
Delete(idpName string) error
3344
// Get retrieves credentials from the store.
3445
// It returns username and secret as strings.
35-
Get(serverURL string) (string, string, error)
46+
Get(idpName string) (string, string, error)
3647
// SupportsCredentialStorage returns true or false if there is credential storage.
3748
SupportsCredentialStorage() bool
3849
}

helper/credentials/saml.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
package credentials
22

33
import (
4-
"path"
5-
64
"github.com/versent/saml2aws/v2/pkg/creds"
75
)
86

97
// LookupCredentials lookup an existing set of credentials and validate it.
10-
func LookupCredentials(loginDetails *creds.LoginDetails, provider string) error {
8+
func LookupCredentials(loginDetails *creds.LoginDetails) error {
119

12-
username, password, err := CurrentHelper.Get(loginDetails.URL)
10+
username, password, err := CurrentHelper.Get(loginDetails.IdpName)
1311
if err != nil {
1412
return err
1513
}
@@ -18,15 +16,17 @@ func LookupCredentials(loginDetails *creds.LoginDetails, provider string) error
1816
loginDetails.Password = password
1917

2018
// If the provider is Okta, check for existing Okta Session Cookie (sid)
21-
if provider == "Okta" {
22-
_, oktaSessionCookie, err := CurrentHelper.Get(loginDetails.URL + "/sessionCookie")
19+
if loginDetails.IdpProvider == "Okta" {
20+
// load up the Okta token from a different secret (idp name + Okta suffix)
21+
_, oktaSessionCookie, err := CurrentHelper.Get(loginDetails.IdpName + OktaSessionCookieSuffix)
2322
if err == nil {
2423
loginDetails.OktaSessionCookie = oktaSessionCookie
2524
}
2625
}
2726

28-
if provider == "OneLogin" {
29-
id, secret, err := CurrentHelper.Get(path.Join(loginDetails.URL, "/auth/oauth2/v2/token"))
27+
if loginDetails.IdpProvider == "OneLogin" {
28+
// load up the one login token from a different secret (idp name + one login suffix)
29+
id, secret, err := CurrentHelper.Get(loginDetails.IdpName + OneLoginTokenSuffix)
3030
if err != nil {
3131
return err
3232
}
@@ -37,9 +37,10 @@ func LookupCredentials(loginDetails *creds.LoginDetails, provider string) error
3737
}
3838

3939
// SaveCredentials save the user credentials.
40-
func SaveCredentials(url, username, password string) error {
40+
func SaveCredentials(idpName, url, username, password string) error {
4141

4242
creds := &Credentials{
43+
IdpName: idpName,
4344
ServerURL: url,
4445
Username: username,
4546
Secret: password,

helper/linuxkeyring/linuxkeyring.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,19 @@ func (kr *KeyringHelper) Add(creds *credentials.Credentials) error {
4141
}
4242

4343
return kr.keyring.Set(keyring.Item{
44-
Key: creds.ServerURL,
44+
Key: credentials.GetKeyFromAccount(creds.IdpName),
4545
Label: credentials.CredsLabel,
4646
Data: encoded,
4747
KeychainNotTrustApplication: false,
4848
})
4949
}
5050

51-
func (kr *KeyringHelper) Delete(serverURL string) error {
52-
return kr.keyring.Remove(serverURL)
51+
func (kr *KeyringHelper) Delete(idpName string) error {
52+
return kr.keyring.Remove(credentials.GetKeyFromAccount(idpName))
5353
}
5454

55-
func (kr *KeyringHelper) Get(serverURL string) (string, string, error) {
56-
item, err := kr.keyring.Get(serverURL)
55+
func (kr *KeyringHelper) Get(idpName string) (string, string, error) {
56+
item, err := kr.keyring.Get(credentials.GetKeyFromAccount(idpName))
5757
if err != nil {
5858
logger.WithField("err", err).Error("keychain Get returned error")
5959
return "", "", credentials.ErrCredentialsNotFound

helper/osxkeychain/osxkeychain.go

+11-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//go:build darwin && cgo
12
// +build darwin,cgo
23

34
package osxkeychain
@@ -18,14 +19,15 @@ type Osxkeychain struct{}
1819

1920
// Add adds new credentials to the keychain.
2021
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
21-
err := h.Delete(creds.ServerURL)
22+
err := h.Delete(creds.IdpName)
2223
if err != nil {
2324
logger.WithError(err).Debug("delete of existing keychain entry failed")
2425
}
2526

2627
item := keychain.NewItem()
2728
item.SetSecClass(keychain.SecClassInternetPassword)
28-
item.SetLabel(credentials.CredsLabel)
29+
item.SetLabel(credentials.GetKeyFromAccount(creds.IdpName))
30+
item.SetString("Purpose", credentials.CredsLabel)
2931
item.SetAccount(creds.Username)
3032
item.SetData([]byte(creds.Secret))
3133
err = splitServer3(creds.ServerURL, item)
@@ -43,36 +45,24 @@ func (h Osxkeychain) Add(creds *credentials.Credentials) error {
4345
}
4446

4547
// Delete removes credentials from the keychain.
46-
func (h Osxkeychain) Delete(serverURL string) error {
48+
func (h Osxkeychain) Delete(idpName string) error {
4749

4850
item := keychain.NewItem()
4951
item.SetSecClass(keychain.SecClassInternetPassword)
50-
err := splitServer3(serverURL, item)
51-
if err != nil {
52-
return err
53-
}
54-
55-
err = keychain.DeleteItem(item)
56-
if err != nil {
57-
return err
58-
}
59-
60-
return nil
52+
item.SetLabel(credentials.GetKeyFromAccount(idpName))
53+
return keychain.DeleteItem(item)
6154
}
6255

6356
// Get returns the username and secret to use for a given registry server URL.
64-
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
57+
func (h Osxkeychain) Get(idpName string) (string, string, error) {
6558

66-
logger.WithField("serverURL", serverURL).Debug("Get credentials")
59+
logger.WithField("Credential Key", idpName).Debug("Get credentials")
6760

6861
query := keychain.NewItem()
6962
query.SetSecClass(keychain.SecClassInternetPassword)
7063

71-
err := splitServer3(serverURL, query)
72-
if err != nil {
73-
return "", "", err
74-
}
75-
64+
// only search on the idp name
65+
query.SetLabel(credentials.GetKeyFromAccount(idpName))
7666
query.SetMatchLimit(keychain.MatchLimitOne)
7767
query.SetReturnAttributes(true)
7868
query.SetReturnData(true)

0 commit comments

Comments
 (0)