diff --git a/config/pingone.go b/config/pingone.go index 6dcc206..a792bda 100644 --- a/config/pingone.go +++ b/config/pingone.go @@ -7,7 +7,6 @@ package config import ( "context" - "crypto/sha256" "fmt" "log/slog" "net/http" @@ -74,8 +73,20 @@ func (c *Configuration) generateTokenKey(grantType svcOAuth2.GrantType) (string, return "", fmt.Errorf("environment ID and client ID are required for token key generation") } - hash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", environmentID, clientID, grantType))) - tokenKey := fmt.Sprintf("token-%x", hash[:8]) + // Optional suffix provided by consumer via Storage.OptionalSuffix. + // When empty, no suffix is appended. + var suffix string + if c.Auth.Storage != nil && strings.TrimSpace(c.Auth.Storage.OptionalSuffix) != "" { + suffix = c.Auth.Storage.OptionalSuffix + } + + // Use SDK oauth2 helper to generate token key, with optional suffix when provided + var tokenKey string + if suffix != "" { + tokenKey = svcOAuth2.GenerateKeychainAccountName(environmentID, clientID, string(grantType), suffix) + } else { + tokenKey = svcOAuth2.GenerateKeychainAccountName(environmentID, clientID, string(grantType)) + } slog.Debug("Generated token key", "environmentID", environmentID, "clientID", clientID, "grantType", grantType, "tokenKey", tokenKey) @@ -178,6 +189,10 @@ type DeviceCode struct { type Storage struct { KeychainName string `envconfig:"PINGONE_STORAGE_NAME" json:"name,omitempty"` Type StorageType `envconfig:"PINGONE_STORAGE_TYPE" json:"type,omitempty"` + // OptionalSuffix allows SDK consumers to append a suffix to the generated + // token key for disambiguation across contexts (e.g., provider/grant/profile). + // If empty, no suffix is appended and the base token key is used. + OptionalSuffix string `envconfig:"PINGONE_STORAGE_OPTIONAL_SUFFIX" json:"optionalSuffix,omitempty"` } // Configuration represents the complete configuration for the PingOne Go Client SDK. @@ -384,7 +399,7 @@ func (c *Configuration) WithUseKeychain(useKeychain bool) *Configuration { } // Set the storage type based on useKeychain value if useKeychain { - c.Auth.Storage.Type = StorageTypeKeychain + c.Auth.Storage.Type = StorageTypeSecureLocal } return c } @@ -405,6 +420,17 @@ func (c *Configuration) WithStorageName(name string) *Configuration { return c } +// WithStorageOptionalSuffix sets an optional suffix for the token key. +// This allows SDK consumers to unify key names across keychain and file storage +// without changing default SDK behavior for other clients. +func (c *Configuration) WithStorageOptionalSuffix(suffix string) *Configuration { + if c.Auth.Storage == nil { + c.Auth.Storage = &Storage{} + } + c.Auth.Storage.OptionalSuffix = suffix + return c +} + // HasBearerToken checks if a static access token is configured. // It returns true if an access token has been set and is not empty, // false otherwise. This is used to determine whether to use static token @@ -494,7 +520,7 @@ func (c *Configuration) TokenSource(ctx context.Context) (oauth2.TokenSource, er // Check keychain for existing valid token before performing auth if c.Auth.GrantType != nil { // Default to storage type of keychain - shouldCheckKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeKeychain + shouldCheckKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeSecureLocal if shouldCheckKeychain { // Validate storage name is set when using keychain @@ -557,7 +583,7 @@ func (c *Configuration) TokenSource(ctx context.Context) (oauth2.TokenSource, er var tokenKey string // Only generate token key if storage is enabled - shouldUseStorage := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeKeychain + shouldUseStorage := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeSecureLocal if shouldUseStorage { var err error tokenKey, err = c.generateTokenKey(*c.Auth.GrantType) @@ -591,7 +617,8 @@ func (c *Configuration) TokenSource(ctx context.Context) (oauth2.TokenSource, er if err != nil { return nil, fmt.Errorf("failed to get token: %w", err) } - shouldSaveToKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeKeychain + shouldSaveToKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeSecureLocal + // Save only when using SECURE_LOCAL storage; skip for NONE/FILE_SYSTEM if shouldSaveToKeychain { if c.Auth.Storage == nil || c.Auth.Storage.KeychainName == "" { return nil, fmt.Errorf("storage name is required when using keychain storage. Use WithStorageName() to set it") @@ -630,7 +657,7 @@ func (c *Configuration) TokenSource(ctx context.Context) (oauth2.TokenSource, er } // Save token to keychain for future use - only if storage type is keychain (or not set) - shouldSaveToKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeKeychain + shouldSaveToKeychain := c.Auth.Storage == nil || c.Auth.Storage.Type == "" || c.Auth.Storage.Type == StorageTypeSecureLocal if shouldSaveToKeychain { // Validate storage name is set when using keychain @@ -670,7 +697,7 @@ func (c *Configuration) TokenSource(ctx context.Context) (oauth2.TokenSource, er } } } else { - slog.Debug("Skipping keychain storage (file storage mode)", "tokenKey", tokenKey) + slog.Debug("Skipping keychain storage (non-secure-local storage)", "tokenKey", tokenKey) } // Set up automatic refresh without keychain persistence if token has refresh capability diff --git a/config/pingone_test.go b/config/pingone_test.go index 02eeb26..525e5a5 100644 --- a/config/pingone_test.go +++ b/config/pingone_test.go @@ -739,8 +739,20 @@ func TestWithStorageType(t *testing.T) { storageType config.StorageType }{ { - name: "StorageTypeKeychain", - storageType: config.StorageTypeKeychain, + name: "StorageTypeSecureLocal", + storageType: config.StorageTypeSecureLocal, + }, + { + name: "StorageTypeFileSystem", + storageType: config.StorageTypeFileSystem, + }, + { + name: "StorageTypeSecureRemote", + storageType: config.StorageTypeSecureRemote, + }, + { + name: "StorageTypeNone", + storageType: config.StorageTypeNone, }, } @@ -766,9 +778,9 @@ func TestWithUseKeychain(t *testing.T) { expectStorageType config.StorageType }{ { - name: "UseKeychain true sets StorageTypeKeychain", + name: "UseKeychain true sets StorageTypeSecureLocal", useKeychain: true, - expectStorageType: config.StorageTypeKeychain, + expectStorageType: config.StorageTypeSecureLocal, }, } diff --git a/config/storage_types.go b/config/storage_types.go index 9af9ff3..6adff7a 100644 --- a/config/storage_types.go +++ b/config/storage_types.go @@ -6,8 +6,14 @@ package config type StorageType string const ( - // StorageTypeKeychain uses the system keychain for token storage - StorageTypeKeychain StorageType = "keychain" + // StorageTypeFileSystem uses the local file system for token storage + StorageTypeFileSystem StorageType = "file_system" + + // StorageTypeSecureLocal uses a secure local storage (e.g., OS keychain) + StorageTypeSecureLocal StorageType = "secure_local" + + // StorageTypeSecureRemote uses a secure remote storage service + StorageTypeSecureRemote StorageType = "secure_remote" // StorageTypeNone defines no token storage StorageTypeNone StorageType = "none" @@ -21,7 +27,11 @@ func (s StorageType) String() string { // IsValid checks if the StorageType is valid func (s StorageType) IsValid() bool { switch s { - case StorageTypeKeychain: + case StorageTypeFileSystem: + return true + case StorageTypeSecureLocal: + return true + case StorageTypeSecureRemote: return true case StorageTypeNone: return true diff --git a/config/storage_types_test.go b/config/storage_types_test.go index d5db961..9d152bf 100644 --- a/config/storage_types_test.go +++ b/config/storage_types_test.go @@ -15,9 +15,24 @@ func TestStorageType_String(t *testing.T) { expected string }{ { - name: "StorageTypeKeychain", - storageType: config.StorageTypeKeychain, - expected: "keychain", + name: "StorageTypeSecureLocal", + storageType: config.StorageTypeSecureLocal, + expected: "secure_local", + }, + { + name: "StorageTypeFileSystem", + storageType: config.StorageTypeFileSystem, + expected: "file_system", + }, + { + name: "StorageTypeSecureRemote", + storageType: config.StorageTypeSecureRemote, + expected: "secure_remote", + }, + { + name: "StorageTypeNone", + storageType: config.StorageTypeNone, + expected: "none", }, } @@ -38,8 +53,23 @@ func TestStorageType_IsValid(t *testing.T) { expected bool }{ { - name: "StorageTypeKeychain is valid", - storageType: config.StorageTypeKeychain, + name: "StorageTypeSecureLocal is valid", + storageType: config.StorageTypeSecureLocal, + expected: true, + }, + { + name: "StorageTypeFileSystem is valid", + storageType: config.StorageTypeFileSystem, + expected: true, + }, + { + name: "StorageTypeSecureRemote is valid", + storageType: config.StorageTypeSecureRemote, + expected: true, + }, + { + name: "StorageTypeNone is valid", + storageType: config.StorageTypeNone, expected: true, }, { @@ -71,7 +101,16 @@ func TestStorageType_IsValid(t *testing.T) { func TestStorageTypeConstants(t *testing.T) { // Verify the constant values are as expected - if config.StorageTypeKeychain != "keychain" { - t.Errorf("Expected StorageTypeKeychain to be 'keychain', got %q", config.StorageTypeKeychain) + if config.StorageTypeSecureLocal != "secure_local" { + t.Errorf("Expected StorageTypeSecureLocal to be 'secure_local', got %q", config.StorageTypeSecureLocal) + } + if config.StorageTypeFileSystem != "file_system" { + t.Errorf("Expected StorageTypeFileSystem to be 'file_system', got %q", config.StorageTypeFileSystem) + } + if config.StorageTypeSecureRemote != "secure_remote" { + t.Errorf("Expected StorageTypeSecureRemote to be 'secure_remote', got %q", config.StorageTypeSecureRemote) + } + if config.StorageTypeNone != "none" { + t.Errorf("Expected StorageTypeNone to be 'none', got %q", config.StorageTypeNone) } } diff --git a/oauth2/keychain_storage.go b/oauth2/keychain_storage.go index bbe461a..35499a0 100644 --- a/oauth2/keychain_storage.go +++ b/oauth2/keychain_storage.go @@ -89,15 +89,26 @@ func (k *KeychainStorage) HasToken() (bool, error) { return true, nil } -// GenerateKeychainAccountName creates a unique account name based on environment ID, client ID, and grant type -func GenerateKeychainAccountName(environmentID, clientID, grantType string) string { +// GenerateKeychainAccountName creates a unique account name based on environment ID, client ID, and grant type. +// Optionally, a suffix can be provided to append to the generated token key for disambiguation across contexts +// (e.g., "_pingone_device_code_default"). Existing callers remain compatible. +func GenerateKeychainAccountName(environmentID, clientID, grantType string, optionalSuffix ...string) string { if environmentID == "" && clientID == "" && grantType == "" { - return "default-token" + // When no inputs are provided, return a stable default (with optional suffix if specified) + base := "default-token" + if len(optionalSuffix) > 0 && optionalSuffix[0] != "" { + return base + optionalSuffix[0] + } + return base } // Create a hash of environment ID + client ID + grant type for uniqueness var b []byte b = fmt.Appendf(b, "%s:%s:%s", environmentID, clientID, grantType) hash := sha256.Sum256(b) - return fmt.Sprintf("token-%x", hash[:8]) // Use first 8 bytes of hash for shorter key + base := fmt.Sprintf("token-%x", hash[:8]) // Use first 8 bytes of hash for shorter key + if len(optionalSuffix) > 0 && optionalSuffix[0] != "" { + return base + optionalSuffix[0] + } + return base }