Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
96464fe
wip
wesleymccollam Sep 29, 2025
32f7d23
working client credentials
wesleymccollam Oct 7, 2025
6020b41
working methods
wesleymccollam Oct 7, 2025
2c8298f
support auth_code, client_credentials, and device_code authentication…
wesleymccollam Oct 16, 2025
968556e
Remove unused OAuth2 artifact code and tests
wesleymccollam Oct 17, 2025
f81f94e
feat(oauth2): Add authorization code flow support
wesleymccollam Oct 30, 2025
bd4d21b
feat(oauth2): Add comprehensive test coverage for OAuth2 flows
wesleymccollam Oct 30, 2025
3000c82
Updates:
wesleymccollam Oct 31, 2025
a367f74
Updates:
wesleymccollam Nov 4, 2025
5ee3d38
update windows open browser command
wesleymccollam Nov 5, 2025
ac14b94
* revert endpoint(s) package changes by adding oauth2/endpoints package
wesleymccollam Nov 5, 2025
ea7891c
* simplify auth code redirect uri values logic
wesleymccollam Nov 5, 2025
b43f62f
Updates:
wesleymccollam Nov 7, 2025
5a14317
Merge branch 'main' into oauth2-auth_code-client_credentials-device_c…
wesleymccollam Nov 7, 2025
e87afe7
prevent slowloris attacks, handle close listener error
wesleymccollam Nov 7, 2025
56f019f
remove unneeded auth_code file
wesleymccollam Nov 7, 2025
f10607c
add missing function and structs from previous commit
wesleymccollam Nov 7, 2025
d0cab1d
lint fixes, html page title update, remove unused client.go file
wesleymccollam Nov 7, 2025
99ad9c9
check server close errors, display success page after token is succes…
wesleymccollam Nov 7, 2025
b4c470d
check error on connection close
wesleymccollam Nov 7, 2025
6f1abd5
only run keychain tests when keychain is available
wesleymccollam Nov 7, 2025
6c7dde8
change auth code to authorization code, use get functions for default…
wesleymccollam Nov 11, 2025
02b4e1f
add missing SG region
wesleymccollam Nov 11, 2025
c2b8fd4
use TopLevelDomain argument in WithRegion method
wesleymccollam Nov 12, 2025
17377e2
add region code enums
wesleymccollam Nov 12, 2025
88348c8
remove unused WithRegion method, remove cli reference in html page
wesleymccollam Nov 13, 2025
d089560
update authentication example docs: rename basic to client credential…
wesleymccollam Nov 13, 2025
e491118
feat: add automatic token refresh with oauth2.ReuseTokenSource
wesleymccollam Nov 17, 2025
5302282
skip keychain tests if keychain is unavailable
wesleymccollam Nov 18, 2025
9d9213e
remove unused region code file
wesleymccollam Nov 18, 2025
d9396b8
add automatic refresh for use without keychain storage
wesleymccollam Nov 19, 2025
58e7366
only generate token key id when keychain storage is enabled
wesleymccollam Nov 19, 2025
4bf3625
update readme for correct app types
wesleymccollam Nov 19, 2025
81ae4eb
Add auto-close countdown and error display improvements to OAuth call…
patrickcping Nov 25, 2025
0627986
Human login embedded http server UX and security improvements (#47)
patrickcping Nov 25, 2025
f2f3547
feat: add headless and custom UX support for OAuth2 authentication fl…
patrickcping Nov 26, 2025
c85bdf2
Merge branch 'main' into oauth2-auth_code-client_credentials-device_c…
patrickcping Nov 26, 2025
527681e
lint issues
patrickcping Nov 26, 2025
82e5280
do not use refresh token for client credentials method
wesleymccollam Dec 1, 2025
7753a75
correctly save client credentials info to keychain
wesleymccollam Dec 1, 2025
23f4dd5
* Browser: tighten URL validation, enforce absolute http/https, rejec…
wesleymccollam Dec 1, 2025
b3a734f
do not require scopes for client credentials
wesleymccollam Dec 2, 2025
2a55f20
remove GOSEC ignore comment from generated file
wesleymccollam Dec 3, 2025
18a5967
Merge branch 'main' into oauth2-auth_code-client_credentials-device_c…
wesleymccollam Dec 3, 2025
f8a276f
add nil check for client credentials scopes
wesleymccollam Dec 3, 2025
027dd63
correct scope test
wesleymccollam Dec 3, 2025
5bea42c
Add state handling tests
patrickcping Dec 4, 2025
ef48654
Add PKCE support and comprehensive documentation to device auth flow
patrickcping Dec 4, 2025
d3cd651
add oidc openid scope enum
wesleymccollam Dec 4, 2025
b63ed22
Merge remote-tracking branch 'refs/remotes/origin/main'
wesleymccollam Dec 9, 2025
b42e3c7
support storage name suffix
wesleymccollam Dec 8, 2025
5d0b67d
add storage type with enums
wesleymccollam Dec 15, 2025
dddbbf6
Merge branch 'main' into oauth2-auth_code-client_credentials-device_c…
wesleymccollam Dec 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 36 additions & 9 deletions config/pingone.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package config

import (
"context"
"crypto/sha256"
"fmt"
"log/slog"
"net/http"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions config/pingone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}

Expand All @@ -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,
},
}

Expand Down
16 changes: 13 additions & 3 deletions config/storage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
53 changes: 46 additions & 7 deletions config/storage_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand All @@ -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,
},
{
Expand Down Expand Up @@ -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)
}
}
19 changes: 15 additions & 4 deletions oauth2/keychain_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}