package config import ( "encoding/json" "errors" "fmt" "os" "path" "path/filepath" "sync" "github.com/google/uuid" "github.com/lestrrat-go/jwx/v3/jwt" ) // ErrConfigFileMissing is thrown when the config.json file is missing. var ErrConfigFileMissing = errors.New("config.json file is missing") // ErrNoAuthenticatedTenants is thrown when the config file has no authenticated tenants. var ErrNoAuthenticatedTenants = errors.New("not logged in. Try `auth0 login`") // Config holds cli configuration settings. type Config struct { onlyOnce sync.Once initError error path string InstallID string `json:"install_id,omitempty"` DefaultTenant string `json:"default_tenant"` Tenants Tenants `json:"tenants"` } // Initialize will load the config settings into memory. func (c *Config) Initialize() error { c.onlyOnce.Do(func() { c.initError = c.loadFromDisk() }) return c.initError } // Validate checks to see if the config is not corrupted, // and we have an authenticated tenant saved. // If we have at least one tenant saved but the DefaultTenant // is empty, it will attempt to set the first available // tenant as the DefaultTenant and save to disk. func (c *Config) Validate() error { if err := c.Initialize(); err != nil { return err } if len(c.Tenants) == 0 { return ErrNoAuthenticatedTenants } if c.DefaultTenant != "" { return nil } for tenant := range c.Tenants { c.DefaultTenant = tenant break // Pick first tenant and exit. } return c.saveToDisk() } // IsLoggedInWithTenant checks if we're logged in with the given tenant. func (c *Config) IsLoggedInWithTenant(tenantName string) bool { // Ignore error as we could // be not logged in yet. _ = c.Initialize() if tenantName == "" { tenantName = c.DefaultTenant } tenant, ok := c.Tenants[tenantName] if !ok { return false } token, err := jwt.ParseInsecure([]byte(tenant.GetAccessToken())) if err != nil { return false } if err = jwt.Validate(token, jwt.WithIssuer("https://auth0.auth0.com/")); err != nil { return false } return true } // GetTenant retrieves all the tenant information from the config. func (c *Config) GetTenant(tenantName string) (Tenant, error) { if err := c.Initialize(); err != nil { return Tenant{}, err } tenant, ok := c.Tenants[tenantName] if !ok { return Tenant{}, fmt.Errorf( "failed to find tenant: %s. Run 'auth0 tenants list' to see your configured tenants "+ "or run 'auth0 login' to configure a new tenant", tenantName, ) } return tenant, nil } // AddTenant adds a tenant to the config. // This is called after a login has completed. func (c *Config) AddTenant(tenant Tenant) error { // Ignore error as we could be // logging in the first time. _ = c.Initialize() c.ensureInstallIDAssigned() if c.DefaultTenant == "" { c.DefaultTenant = tenant.Domain } if c.Tenants == nil { c.Tenants = make(map[string]Tenant) } c.Tenants[tenant.Domain] = tenant return c.saveToDisk() } // RemoveTenant removes a tenant from the config. // This is called after a logout has completed. func (c *Config) RemoveTenant(tenant string) error { if err := c.Initialize(); err != nil { if errors.Is(err, ErrConfigFileMissing) { return nil // Config file is missing, so nothing to remove. } return err } if c.DefaultTenant == "" && len(c.Tenants) == 0 { return nil // Nothing to remove. } if c.DefaultTenant != "" && len(c.Tenants) == 0 { c.DefaultTenant = "" // Reset possible corruption of config file. return c.saveToDisk() } delete(c.Tenants, tenant) if c.DefaultTenant == tenant { c.DefaultTenant = "" for otherTenant := range c.Tenants { c.DefaultTenant = otherTenant break // Pick first tenant and exit as we called delete above. } } return c.saveToDisk() } // ListAllTenants retrieves a list with all configured tenants. func (c *Config) ListAllTenants() ([]Tenant, error) { if err := c.Initialize(); err != nil { return nil, err } tenants := make([]Tenant, 0, len(c.Tenants)) for _, tenant := range c.Tenants { tenants = append(tenants, tenant) } return tenants, nil } // SetDefaultTenant saves the new default tenant to the disk. func (c *Config) SetDefaultTenant(tenantName string) error { tenant, err := c.GetTenant(tenantName) if err != nil { return err } c.DefaultTenant = tenant.Domain return c.saveToDisk() } // SetDefaultAppIDForTenant saves the new default app id for the tenant to the disk. func (c *Config) SetDefaultAppIDForTenant(tenantName, appID string) error { tenant, err := c.GetTenant(tenantName) if err != nil { return err } tenant.DefaultAppID = appID c.Tenants[tenant.Domain] = tenant return c.saveToDisk() } func (c *Config) ensureInstallIDAssigned() { if c.InstallID != "" { return } c.InstallID = uuid.NewString() } func (c *Config) loadFromDisk() error { if c.path == "" { c.path = defaultPath() } if _, err := os.Stat(c.path); os.IsNotExist(err) { return ErrConfigFileMissing } buffer, err := os.ReadFile(c.path) if err != nil { return err } return json.Unmarshal(buffer, c) } func (c *Config) saveToDisk() error { dir := filepath.Dir(c.path) if _, err := os.Stat(dir); os.IsNotExist(err) { const dirPerm os.FileMode = 0700 // Directory permissions (read, write, and execute for the owner only). if err := os.MkdirAll(dir, dirPerm); err != nil { return err } } buffer, err := json.MarshalIndent(c, "", " ") if err != nil { return err } const filePerm os.FileMode = 0600 // File permissions (read and write for the owner only). return os.WriteFile(c.path, buffer, filePerm) } func defaultPath() string { return path.Join(os.Getenv("HOME"), ".config", "auth0", "config.json") }