diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1b2ffe1f4..f87554b2f 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -32,7 +32,7 @@ var requiredScopes = []string{ "create:rules", "delete:rules", "read:rules", "update:rules", "read:users", "update:users", "read:branding", "update:branding", - "read:client_keys", "read:logs", "read:tenant_settings", + "read:client_keys", "read:logs", "read:tenant_settings", "read:custom_domains", } // RequiredScopes returns the scopes used for login. diff --git a/internal/auth0/auth0.go b/internal/auth0/auth0.go index 571fe3a5a..e5d846b1f 100644 --- a/internal/auth0/auth0.go +++ b/internal/auth0/auth0.go @@ -14,6 +14,7 @@ type API struct { ActionBinding ActionBindingAPI Branding BrandingAPI Client ClientAPI + CustomDomain CustomDomainAPI Log LogAPI ResourceServer ResourceServerAPI Rule RuleAPI @@ -29,6 +30,7 @@ func NewAPI(m *management.Management) *API { ActionBinding: m.ActionBinding, Branding: m.Branding, Client: m.Client, + CustomDomain: m.CustomDomain, Log: m.Log, ResourceServer: m.ResourceServer, Rule: m.Rule, diff --git a/internal/auth0/custom_domain.go b/internal/auth0/custom_domain.go new file mode 100644 index 000000000..c559b1e62 --- /dev/null +++ b/internal/auth0/custom_domain.go @@ -0,0 +1,8 @@ +//go:generate mockgen -source=custom_domain.go -destination=custom_domain_mock.go -package=auth0 +package auth0 + +import "gopkg.in/auth0.v5/management" + +type CustomDomainAPI interface { + List(opts ...management.RequestOption) (c []*management.CustomDomain, err error) +} diff --git a/internal/cli/template.go b/internal/cli/template.go index 772e96191..4321228da 100644 --- a/internal/cli/template.go +++ b/internal/cli/template.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "github.com/auth0/auth0-cli/internal/ansi" @@ -27,6 +28,8 @@ var ( {"Login box + image", branding.ImageTemplate}, {"Page footers", branding.FooterTemplate}, } + + errNotAllowed = errors.New("This feature requires at least one custom domain to be configured for the tenant.") ) func brandingCmd(cli *cli) *cobra.Command { @@ -178,6 +181,34 @@ func (cli *cli) obtainCustomTemplateData(ctx context.Context) (*branding.Templat tenant *management.Tenant ) + g.Go(func() error { + var err error + domains, err := cli.api.CustomDomain.List() + if err != nil { + errStatus := err.(management.Error) + // 403 is a valid response for free tenants that don't have + // custom domains enabled + if errStatus != nil && errStatus.Status() == 403 { + return errNotAllowed + } + + return err + } + + for _, domain := range domains { + if domain.GetStatus() == "ready" { + return nil + } + } + return errNotAllowed + }) + + g.Go(func() error { + var err error + clients, err = cli.api.Client.List(management.Context(ctx), management.ExcludeFields(clientExcludedList...)) + return err + }) + g.Go(func() error { var err error clients, err = cli.api.Client.List(management.Context(ctx), management.ExcludeFields(clientExcludedList...)) diff --git a/internal/cli/test.go b/internal/cli/test.go index f2f081eaf..e7035230f 100644 --- a/internal/cli/test.go +++ b/internal/cli/test.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "github.com/auth0/auth0-cli/internal/ansi" @@ -50,6 +51,20 @@ var ( ShortForm: "s", Help: "The list of scope you want to use to generate the token.", } + + testDomainArg = Argument{ + Name: "Custom Domain", + Help: "One of your custom domains.", + } + + testDomain = Flag{ + Name: "Custom Domain", + LongForm: "domain", + ShortForm: "d", + Help: "One of your custom domains.", + } + + errNoCustomDomains = errors.New("there are currently no custom domains") ) func testCmd(cli *cli) *cobra.Command { @@ -71,6 +86,7 @@ func testLoginCmd(cli *cli) *cobra.Command { ClientID string Audience string ConnectionName string + CustomDomain string } cmd := &cobra.Command{ @@ -123,6 +139,15 @@ auth0 test login --connection `, return fmt.Errorf("Unable to find client %s; if you specified a client, please verify it exists, otherwise re-run the command", inputs.ClientID) } + if inputs.CustomDomain == "" { + err = testDomainArg.Pick(cmd, &inputs.CustomDomain, cli.customDomainPickerOptions) + if err != nil { + if err != errNoCustomDomains { + return err + } + } + } + if proceed := runLoginFlowPreflightChecks(cli, client); !proceed { return nil } @@ -135,6 +160,7 @@ auth0 test login --connection `, inputs.Audience, // audience is only supported for the test token command "login", // force a login page when using the test login command cliLoginTestingScopes, + inputs.CustomDomain, ) if err != nil { return fmt.Errorf("An unexpected error occurred while logging in to client %s: %w", inputs.ClientID, err) @@ -172,6 +198,7 @@ auth0 test login --connection `, cmd.SetUsageTemplate(resourceUsageTemplate()) testAudience.RegisterString(cmd, &inputs.Audience, "") testConnection.RegisterString(cmd, &inputs.ConnectionName, "") + testDomain.RegisterString(cmd, &inputs.CustomDomain, "") return cmd } @@ -246,6 +273,7 @@ auth0 test token --client-id --audience --scopes inputs.Audience, "", // We don't want to force a prompt for the test token command inputs.Scopes, + "", ) if err != nil { return fmt.Errorf("An unexpected error occurred when logging in to client %s: %w", inputs.ClientID, err) @@ -274,3 +302,40 @@ func cleanupTempApplication(isTemp bool, cli *cli, id string) { cli.renderer.Infof("Default test application removed") } } + +func (c *cli) customDomainPickerOptions() (pickerOptions, error) { + var opts pickerOptions + + domains, err := c.api.CustomDomain.List() + if err != nil { + errStatus := err.(management.Error) + // 403 is a valid response for free tenants that don't have + // custom domains enabled + if errStatus != nil && errStatus.Status() == 403 { + return nil, errNoCustomDomains + } + + return nil, err + } + + tenant, err := c.getTenant() + if err != nil { + return nil, err + } + + for _, d := range domains { + if d.GetStatus() != "ready" { + continue + } + + opts = append(opts, pickerOption{value: d.GetDomain(), label: d.GetDomain()}) + } + + if len(opts) == 0 { + return nil, errNoCustomDomains + } + + opts = append(opts, pickerOption{value: "", label: fmt.Sprintf("none (use %s)", tenant.Domain)}) + + return opts, nil +} diff --git a/internal/cli/utils_shared.go b/internal/cli/utils_shared.go index 1e9a8cb51..10cf6bed4 100644 --- a/internal/cli/utils_shared.go +++ b/internal/cli/utils_shared.go @@ -108,7 +108,7 @@ func runLoginFlowPreflightChecks(cli *cli, c *management.Client) (abort bool) { // runLoginFlow initiates a full user-facing login flow, waits for a response // and returns the retrieved tokens to the caller when done. -func runLoginFlow(cli *cli, t tenant, c *management.Client, connName, audience, prompt string, scopes []string) (*authutil.TokenResponse, error) { +func runLoginFlow(cli *cli, t tenant, c *management.Client, connName, audience, prompt string, scopes []string, customDomain string) (*authutil.TokenResponse, error) { var tokenResponse *authutil.TokenResponse err := ansi.Spinner("Waiting for login flow to complete", func() error { @@ -122,8 +122,13 @@ func runLoginFlow(cli *cli, t tenant, c *management.Client, connName, audience, return err } + domain := t.Domain + if customDomain != "" { + domain = customDomain + } + // Build a login URL and initiate login in a browser window. - loginURL, err := authutil.BuildLoginURL(t.Domain, c.GetClientID(), cliLoginTestingCallbackURL, state, connName, audience, prompt, scopes) + loginURL, err := authutil.BuildLoginURL(domain, c.GetClientID(), cliLoginTestingCallbackURL, state, connName, audience, prompt, scopes) if err != nil { return err }