Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DXCDT-298: Interactive login prompt (3/x) #551

Merged
merged 19 commits into from
Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ test-unit: ## Run unit tests
test-integration: $(GO_BIN)/commander ## Run integration tests. To run a specific test pass the FILTER var. Usage: `make test-integration FILTER="attack protection"`
${call print, "Running integration tests"}
@$(MAKE) install # ensure fresh install prior to running test
auth0 tenants add ${AUTH0_CLI_CLIENT_DOMAIN} --client-id ${AUTH0_CLI_CLIENT_ID} --client-secret ${AUTH0_CLI_CLIENT_SECRET} && commander test ./test/integration/test-cases.yaml --filter "$(FILTER)"; \
auth0 login --domain ${AUTH0_CLI_CLIENT_DOMAIN} --client-id ${AUTH0_CLI_CLIENT_ID} --client-secret ${AUTH0_CLI_CLIENT_SECRET} && commander test ./test/integration/test-cases.yaml --filter "$(FILTER)"; \
exit_code=$$?; \
bash ./test/integration/scripts/test-cleanup.sh; \
exit $$exit_code
Expand Down
16 changes: 14 additions & 2 deletions docs/auth0_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@ Authenticate the Auth0 CLI

### Synopsis

Sign in to your Auth0 account and authorize the CLI to access the Management API.
Authenticates the Auth0 CLI either as a user using personal credentials or as a machine using client credentials (client ID/secret).

```
auth0 login [flags]
```

### Examples

```

auth0 login
auth0 login --domain <tenant-domain> --client-id <client-id> --client-secret <client-secret>

```

### Options

```
-h, --help help for login
-i, --client-id string Client ID of the application.
-s, --client-secret string Client Secret of the application.
--domain string Specifies tenant domain when authenticating via client credentials (client ID, client secret)
-h, --help help for login
```

### Options inherited from parent commands
Expand Down
1 change: 0 additions & 1 deletion docs/auth0_tenants.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Manage configured tenants.
### SEE ALSO

* [auth0](/auth0-cli/) - Supercharge your development workflow.
* [auth0 tenants add](auth0_tenants_add.md) - Add a tenant with client credentials
* [auth0 tenants list](auth0_tenants_list.md) - List your tenants
* [auth0 tenants open](auth0_tenants_open.md) - Open tenant settings page in the Auth0 Dashboard
* [auth0 tenants use](auth0_tenants_use.md) - Set the active tenant
Expand Down
41 changes: 0 additions & 41 deletions docs/auth0_tenants_add.md

This file was deleted.

17 changes: 11 additions & 6 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,23 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) {
return Tenant{}, err
}

if t.AccessToken == "" || (scopesChanged(t) && t.authenticatedWithDeviceCodeFlow()) {
return RunLogin(ctx, c, true)
if scopesChanged(t) && t.authenticatedWithDeviceCodeFlow() {
c.renderer.Warnf("Required scopes have changed. Please log in to re-authorize the CLI.")
return RunLoginAsUser(ctx, c)
}

if !t.hasExpiredToken() {
if t.AccessToken != "" && !t.hasExpiredToken() {
return t, nil
}

if err := t.regenerateAccessToken(ctx, c); err != nil {
// Ask and guide the user through the login process.
c.renderer.Errorf("failed to renew access token, %s", err)
return RunLogin(ctx, c, true)
if t.authenticatedWithClientCredentials() {
return t, fmt.Errorf("Failed to renew access token. This may occur if the designated application has been deleted or client secret has been rotated. Please re-authenticate by running `auth0 login --as-machine`")
}

c.renderer.Warnf("Failed to renew access token. Please log in to re-authorize the CLI.")
return RunLoginAsUser(ctx, c)

}

if err := c.addTenant(t); err != nil {
Expand Down
162 changes: 141 additions & 21 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,139 @@ import (
"github.com/auth0/auth0-cli/internal/prompt"
)

var (
loginTenantDomain = Flag{
Name: "Tenant Domain",
LongForm: "domain",
Help: "Specifies tenant domain when authenticating via client credentials (client ID, client secret)",
IsRequired: false,
}

loginClientID = Flag{
Name: "Client ID",
LongForm: "client-id",
Help: "Client ID of the application.",
IsRequired: false,
}

loginClientSecret = Flag{
Name: "Client Secret",
LongForm: "client-secret",
Help: "Client Secret of the application.",
IsRequired: false,
}

loginAsUser = Flag{
Name: "Login as user",
LongForm: "as-user",
Help: "Initializes login as a user via device code flow.",
IsRequired: false,
willvedd marked this conversation as resolved.
Show resolved Hide resolved
}
)

type LoginInputs struct {
Domain string
ClientID string
ClientSecret string
LoginAsUser bool
}

func loginCmd(cli *cli) *cobra.Command {
var inputs LoginInputs

cmd := &cobra.Command{
Use: "login",
Args: cobra.NoArgs,
Short: "Authenticate the Auth0 CLI",
Long: "Sign in to your Auth0 account and authorize the CLI to access the Management API.",
Long: "Authenticates the Auth0 CLI either as a user using personal credentials or as a machine using client credentials (client ID/secret).",
Example: `
auth0 login
auth0 login --domain <tenant-domain> --client-id <client-id> --client-secret <client-secret>
auth0 login --as-user
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if _, err := RunLogin(ctx, cli, false); err != nil {
return err

skipToMachineLogin := inputs.ClientID != "" || inputs.ClientSecret != "" || inputs.Domain != ""
skipToUserLogin := inputs.LoginAsUser

shouldPrompt := !skipToMachineLogin && !skipToUserLogin

var selectedType string

interactivePromptOptions := struct {
User string
Machine string
}{
User: "As a user",
Machine: "As a machine",
}

if shouldPrompt {
cli.renderer.Output(fmt.Sprintf("%s\n\n%s\n%s\n\n%s\n%s\n%s\n%s\n\n",
ansi.Bold("✪ Welcome to the Auth0 CLI 🎊"),
"An Auth0 tenant is required to operate this CLI.",
"To create one, visit: https://auth0.com/signup.",
"You may authenticate to your tenant either as a user with personal",
"credentials or as a machine via client credentials. For more",
"information about authenticating the CLI to your tenant, visit",
"the docs: https://auth0.github.io/auth0-cli/auth0_login.html",
))

input := prompt.SelectInput("auth-type", "How would you like to authenticate?", "Authenticating as a user is recommended if performing ad-hoc operations or working locally.\nAlternatively, authenticating as a machine is recommended for automated workflows (ex:CI).\n",
[]string{interactivePromptOptions.User, interactivePromptOptions.Machine}, interactivePromptOptions.User, shouldPrompt)
if err := prompt.AskOne(input, &selectedType); err != nil {
return handleInputError(err)
}
}

shouldLoginAsUser := skipToUserLogin || selectedType == interactivePromptOptions.User

if shouldLoginAsUser {
if _, err := RunLoginAsUser(ctx, cli); err != nil {
return err
}
} else {
if err := RunLoginAsMachine(ctx, inputs, cli, cmd); err != nil {
return err
}
}

cli.renderer.Infof("Successfully authenticated to %s", inputs.Domain)
cli.tracker.TrackCommandRun(cmd, cli.config.InstallID)

return nil
},
}

loginTenantDomain.RegisterString(cmd, &inputs.Domain, "")
loginClientID.RegisterString(cmd, &inputs.ClientID, "")
loginClientSecret.RegisterString(cmd, &inputs.ClientSecret, "")
cmd.MarkFlagsRequiredTogether("client-id", "client-secret", "domain")
loginAsUser.RegisterBool(cmd, &inputs.LoginAsUser, false)

cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = cmd.Flags().MarkHidden("tenant")
_ = cmd.Flags().MarkHidden("json")
_ = cmd.Flags().MarkHidden("no-input")
cmd.Parent().HelpFunc()(cmd, args)
})

return cmd
}

// RunLogin runs the login flow guiding the user through the process
// RunLoginAsUser runs the login flow guiding the user through the process
// by showing the login instructions, opening the browser.
// Use `expired` to run the login from other commands setup:
// this will only affect the messages.
func RunLogin(ctx context.Context, cli *cli, expired bool) (Tenant, error) {
message := fmt.Sprintf(
"%s\n\n%s\n\n",
"✪ Welcome to the Auth0 CLI 🎊",
"If you don't have an account, please create one here: https://auth0.com/signup.",
)

if expired {
message = "Please sign in to re-authorize the CLI."
cli.renderer.Warnf(message)
} else {
cli.renderer.Output(message)
}

func RunLoginAsUser(ctx context.Context, cli *cli) (Tenant, error) {
state, err := cli.authenticator.Start(ctx)
if err != nil {
return Tenant{}, fmt.Errorf("Failed to start the authentication process: %w.", err)
}

message = fmt.Sprintf("Your device confirmation code is: %s\n\n", ansi.Bold(state.UserCode))
message := fmt.Sprintf("\n%s\n%s%s\n\n",
"A browser window needs to be opened to complete authentication.",
"Note you device confirmation code: ",
ansi.Bold(state.UserCode))
cli.renderer.Output(message)

if cli.noInput {
Expand Down Expand Up @@ -141,3 +223,41 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (Tenant, error) {

return tenant, nil
}

// 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, nil); err != nil {
return err
}

token, err := auth.GetAccessTokenFromClientCreds(auth.ClientCredentials{
ClientID: inputs.ClientID,
ClientSecret: inputs.ClientSecret,
Domain: inputs.Domain,
})
if err != nil {
return err
}

t := Tenant{
Domain: inputs.Domain,
AccessToken: token.AccessToken,
ExpiresAt: token.ExpiresAt,
ClientID: inputs.ClientID,
ClientSecret: inputs.ClientSecret,
}

if err := cli.addTenant(t); err != nil {
return fmt.Errorf("unexpected error when attempting to save tenant data: %w", err)
}

return nil
}
Loading