diff --git a/README.md b/README.md index b04a739c5..7941c0219 100644 --- a/README.md +++ b/README.md @@ -136,8 +136,39 @@ There are two ways to authenticate: - **As a user** - Recommended when invoking on a personal machine or other interactive environment. Facilitated by [device authorization](https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow) flow and cannot be used for private cloud tenants. - **As a machine** - Recommended when running on a server or non-interactive environments (ex: CI, authenticating to a **private cloud**). Facilitated by [client credentials](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow) flow. Flags available for bypassing interactive shell. +- **Profile-based login:** Use `--profile` to easily switch between multiple Auth0 tenants or environments (e.g., `default`, `dev`, etc.) +--- + +### Configure Your Credentials + +Create a credentials file at `~/.auth0/credentials` (or set the `AUTH0_CREDENTIALS_FILE` environment variable to use a custom path). + +The file should use **INI format** and can contain multiple profiles. +**Example:** + +```ini +[default] +domain = your-tenant.auth0.com +client_id = YOUR_CLIENT_ID +client_secret = YOUR_CLIENT_SECRET + +[dev] +domain = dev-tenant.auth0.com +client_id = YOUR_DEV_CLIENT_ID +client_secret = YOUR_DEV_CLIENT_SECRET +``` +- Each section name (e.g., `[default]`, `[dev]`) is a **tenant-profile**. +- You can add as many tenant profiles as needed for different tenants/environments. + +--- + +## Environment Variables + +- `AUTH0_CREDENTIALS_FILE`: Path to the credentials file (defaults to `~/.auth0/credentials`). + +--- > **Warning** > Authenticating as a user is not supported for **private cloud** tenants. diff --git a/docs/auth0_login.md b/docs/auth0_login.md index 801c38462..8a139c80a 100644 --- a/docs/auth0_login.md +++ b/docs/auth0_login.md @@ -22,6 +22,7 @@ auth0 login [flags] auth0 login auth0 login --domain --client-id --client-secret auth0 login --scopes "read:client_grants,create:client_grants" + auth0 login --profile ``` @@ -31,6 +32,7 @@ auth0 login [flags] --client-id string Client ID of the application when authenticating via client credentials. --client-secret string Client secret of the application when authenticating via client credentials. --domain string Tenant domain of the application when authenticating via client credentials. + --profile string Tenant Profile Label name to load Auth0 credentials from. If not provided, the default profile will be used. --scopes strings Additional scopes to request when authenticating via device code flow. By default, only scopes for first-class functions are requested. Primarily useful when using the api command to execute arbitrary Management API requests. ``` diff --git a/go.mod b/go.mod index 8159f4978..e94cb4140 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( golang.org/x/sys v0.33.0 golang.org/x/term v0.32.0 golang.org/x/text v0.25.0 + gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index cade8cfc1..e635fcd8b 100644 --- a/go.sum +++ b/go.sum @@ -315,6 +315,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/dnaeon/go-vcr.v3 v3.2.0 h1:Rltp0Vf+Aq0u4rQXgmXgtgoRDStTnFN83cWgSGSoRzM= gopkg.in/dnaeon/go-vcr.v3 v3.2.0/go.mod h1:2IMOnnlx9I6u9x+YBsM3tAMx6AlOxnJ0pWxQAzZ79Ag= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/cli/login.go b/internal/cli/login.go index 72f8a6b77..860d5d5e3 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -4,8 +4,12 @@ import ( "context" "fmt" "net/http" + "os" + "path/filepath" "strings" + "gopkg.in/ini.v1" + "github.com/pkg/browser" "github.com/spf13/cobra" @@ -48,6 +52,14 @@ var ( IsRequired: false, AlwaysPrompt: false, } + + loginTenantProfileLabel = Flag{ + Name: "Tenant Profile Label", + LongForm: "profile", + Help: "Tenant Profile Label name to load Auth0 credentials from. If not provided, the default profile will be used.", + IsRequired: false, + AlwaysPrompt: false, + } ) type LoginInputs struct { @@ -55,14 +67,46 @@ type LoginInputs struct { ClientID string ClientSecret string AdditionalScopes []string + TenantProfile string } func (i *LoginInputs) isLoggingInWithAdditionalScopes() bool { return len(i.AdditionalScopes) > 0 } +func loadAuth0Credentials(profile string, inputs *LoginInputs) error { + credPath := os.Getenv("AUTH0_CREDENTIALS_FILE") + if credPath == "" { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + credPath = filepath.Join(home, ".auth0", "credentials") + } + + cfg, err := ini.Load(credPath) + if err != nil { + return err + } + + if !cfg.HasSection(profile) { + return fmt.Errorf("profile %q not found", profile) + } + + sec := cfg.Section(profile) + + inputs.Domain = sec.Key("domain").String() + inputs.ClientID = sec.Key("client_id").String() + inputs.ClientSecret = sec.Key("client_secret").String() + + return nil +} + func loginCmd(cli *cli) *cobra.Command { - var inputs LoginInputs + var ( + inputs LoginInputs + ) cmd := &cobra.Command{ Use: "login", @@ -74,8 +118,16 @@ func loginCmd(cli *cli) *cobra.Command { "this is the recommended method for Private Cloud users.\n\n", Example: ` auth0 login auth0 login --domain --client-id --client-secret - auth0 login --scopes "read:client_grants,create:client_grants"`, + auth0 login --scopes "read:client_grants,create:client_grants" + auth0 login --profile `, RunE: func(cmd *cobra.Command, args []string) error { + if inputs.TenantProfile != "" { + err := loadAuth0Credentials(inputs.TenantProfile, &inputs) + if err != nil { + return fmt.Errorf("failed to load auth0 credentials from %q: %v", inputs.TenantProfile, err) + } + } + var selectedLoginType string const loginAsUser, loginAsMachine = "As a user", "As a machine" shouldLoginAsUser, shouldLoginAsMachine := false, false @@ -189,6 +241,7 @@ func loginCmd(cli *cli) *cobra.Command { loginTenantDomain.RegisterString(cmd, &inputs.Domain, "") loginClientID.RegisterString(cmd, &inputs.ClientID, "") loginClientSecret.RegisterString(cmd, &inputs.ClientSecret, "") + loginTenantProfileLabel.RegisterString(cmd, &inputs.TenantProfile, "") loginAdditionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, []string{}) cmd.MarkFlagsMutuallyExclusive("client-id", "scopes") cmd.MarkFlagsMutuallyExclusive("client-secret", "scopes") @@ -317,16 +370,11 @@ func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string, do // RunLoginAsMachine facilitates the authentication process using client credentials (client ID, client secret). func RunLoginAsMachine(ctx context.Context, inputs LoginInputs, cli *cli, cmd *cobra.Command) error { - if err := loginTenantDomain.Ask(cmd, &inputs.Domain, nil); err != nil { - return err - } - - if err := loginClientID.Ask(cmd, &inputs.ClientID, nil); err != nil { - return err - } - - if err := loginClientSecret.AskPassword(cmd, &inputs.ClientSecret); err != nil { - return err + if inputs.TenantProfile == "" { + err := promptForCredentials(cmd, &inputs) + if err != nil { + return err + } } token, err := auth.GetAccessTokenFromClientCreds( @@ -371,3 +419,19 @@ func RunLoginAsMachine(ctx context.Context, inputs LoginInputs, cli *cli, cmd *c return nil } + +func promptForCredentials(cmd *cobra.Command, inputs *LoginInputs) error { + if err := loginTenantDomain.Ask(cmd, &inputs.Domain, nil); err != nil { + return err + } + + if err := loginClientID.Ask(cmd, &inputs.ClientID, nil); err != nil { + return err + } + + if err := loginClientSecret.AskPassword(cmd, &inputs.ClientSecret); err != nil { + return err + } + + return nil +}