Skip to content

Commit 76bbe0e

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 68d09f2 commit 76bbe0e

File tree

13 files changed

+128
-74
lines changed

13 files changed

+128
-74
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"
@@ -83,7 +83,7 @@ func ListRoles(loginFlags *flags.LoginExecFlags) error {
8383
}
8484

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

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

+53-11
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ import (
66
"time"
77

88
"github.com/stretchr/testify/assert"
9-
"github.com/versent/saml2aws/v2"
9+
10+
saml2aws "github.com/versent/saml2aws/v2"
1011
"github.com/versent/saml2aws/v2/pkg/awsconfig"
1112
"github.com/versent/saml2aws/v2/pkg/cfg"
1213
"github.com/versent/saml2aws/v2/pkg/creds"
1314
"github.com/versent/saml2aws/v2/pkg/flags"
1415
)
1516

1617
func TestResolveLoginDetailsWithFlags(t *testing.T) {
18+
commonFlags := &flags.CommonFlags{
19+
URL: "https://id.example.com",
20+
Username: "wolfeidau",
21+
Password: "testtestlol",
22+
MFAToken: "123456",
23+
SkipPrompt: true,
24+
}
1725

18-
commonFlags := &flags.CommonFlags{URL: "https://id.example.com", Username: "wolfeidau", Password: "testtestlol", MFAIPAddress: "127.0.0.1", MFAToken: "123456", SkipPrompt: true}
1926
loginFlags := &flags.LoginExecFlags{CommonFlags: commonFlags}
2027

2128
idpa := &cfg.IDPAccount{
29+
Name: "AccountName",
2230
URL: "https://id.example.com",
2331
MFA: "none",
2432
Provider: "Ping",
@@ -27,16 +35,30 @@ func TestResolveLoginDetailsWithFlags(t *testing.T) {
2735
loginDetails, err := resolveLoginDetails(idpa, loginFlags)
2836

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

3349
func TestOktaResolveLoginDetailsWithFlags(t *testing.T) {
34-
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,24 +69,45 @@ 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)
63-
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)
64108
}
65109

66110
func TestResolveRoleSingleEntry(t *testing.T) {
67-
68111
adminRole := &saml2aws.AWSRole{
69112
Name: "admin",
70113
RoleARN: "arn:aws:iam::456456456456:saml-provider/example-idp,arn:aws:iam::456456456456:role/admin",
@@ -81,7 +124,6 @@ func TestResolveRoleSingleEntry(t *testing.T) {
81124
}
82125

83126
func TestCredentialsToCredentialProcess(t *testing.T) {
84-
85127
aws_creds := &awsconfig.AWSCredentials{
86128
AWSAccessKey: "someawsaccesskey",
87129
AWSSecretKey: "somesecretkey",

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_linux.go

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

5454
return kr.keyring.Set(keyring.Item{
55-
Key: creds.ServerURL,
55+
Key: credentials.GetKeyFromAccount(creds.IdpName),
5656
Label: credentials.CredsLabel,
5757
Data: encoded,
5858
KeychainNotTrustApplication: false,
5959
})
6060
}
6161

62-
func (kr *KeyringHelper) Delete(serverURL string) error {
63-
return kr.keyring.Remove(serverURL)
62+
func (kr *KeyringHelper) Delete(idpName string) error {
63+
return kr.keyring.Remove(credentials.GetKeyFromAccount(idpName))
6464
}
6565

66-
func (kr *KeyringHelper) Get(serverURL string) (string, string, error) {
67-
item, err := kr.keyring.Get(serverURL)
66+
func (kr *KeyringHelper) Get(idpName string) (string, string, error) {
67+
item, err := kr.keyring.Get(credentials.GetKeyFromAccount(idpName))
6868
if err != nil {
6969
logger.WithField("err", err).Error("keychain Get returned error")
7070
return "", "", credentials.ErrCredentialsNotFound

0 commit comments

Comments
 (0)