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

fix: UX updates for try-login #49

Merged
merged 1 commit into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
143 changes: 108 additions & 35 deletions internal/cli/try_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/auth0/auth0-cli/internal/auth"
"github.com/auth0/auth0-cli/internal/auth0"
"github.com/auth0/auth0-cli/internal/open"
"github.com/auth0/auth0-cli/internal/prompt"
"github.com/spf13/cobra"
"gopkg.in/auth0.v5/management"
)
Expand Down Expand Up @@ -39,48 +40,52 @@ Launch a browser to try out your universal login box for the given client.
var userInfo *auth.UserInfo
var tokenResponse *auth.TokenResponse

err := ansi.Spinner("Trying login", func() error {
var err error
tenant, err := cli.getTenant()
tenant, err := cli.getTenant()
if err != nil {
return err
}

// use the client ID as passed in by the user, or default to the
// "CLI Login Testing" client if none passed. This client is only
// used for testing login from the CLI and will be created if it
// does not exist.
if clientID == "" {
client, err := getOrCreateCLITesterClient(cli.api.Client)
if err != nil {
return err
}
clientID = client.GetClientID()
}

// use the client ID as passed in by the user, or default to the
// "CLI Login Testing" client if none passed. This client is only
// used for testing login from the CLI and will be created if it
// does not exist.
if clientID == "" {
client, err := getOrCreateCLITesterClient(cli.api.Client)
if err != nil {
return err
}
clientID = client.GetClientID()
}
client, err := cli.api.Client.Read(clientID)
if err != nil {
return err
}

client, err := cli.api.Client.Read(clientID)
if err != nil {
return err
}
cli.renderer.Infof("A browser window will open to begin this client's login flow.")
cli.renderer.Infof("Once login is complete, you can return to the CLI to view user profile information and tokens.\n")

// check if the chosen client includes our local callback URL in its
// allowed list. If not we'll need to add it (after asking the user
// for permission).
needsLocalCallbackURL := !checkForLocalCallbackURL(client)
if needsLocalCallbackURL {
cli.renderer.Warnf("The client you are using does not currently allow callbacks to localhost.")
cli.renderer.Warnf("To complete the login flow the CLI needs to redirect logins to a local server and record the result.\n")
cli.renderer.Warnf("The client will be modified to update the allowed callback URLs, we'll remove them when done.")
cli.renderer.Warnf("If you do not wish to modify the client, you can abort now.\n")
}

// check if the client's initiate_login_uri matches the one for our
// "CLI Login Testing" app. If so, then initiate the login via the
// `/authorize` endpoint, if not, open a browser at the client's
// configured URL. If none is specified, return an error to the
// caller explaining the problem.
if client.GetInitiateLoginURI() == "" {
return fmt.Errorf(
"client %s does not specify a URL with which to initiate login",
client.GetClientID(),
)
}
if confirmed := prompt.Confirm("Do you wish to proceed?"); !confirmed {
return nil
}
fmt.Fprint(cli.renderer.MessageWriter, "\n")

if client.GetInitiateLoginURI() != cliLoginTestingInitiateLoginURI {
if connectionName != "" {
cli.renderer.Warnf("Specific connections are not supported when using a non-default client, ignoring.")
cli.renderer.Warnf("You should ensure the connection you wish to test is enabled for the client you want to use in the Auth0 Dashboard.")
err = ansi.Spinner("Waiting for login flow to complete", func() error {
if needsLocalCallbackURL {
if err := addLocalCallbackURLToClient(cli.api.Client, client); err != nil {
return err
}
return open.URL(client.GetInitiateLoginURI())
}

// Build a login URL and initiate login in a browser window.
Expand Down Expand Up @@ -116,14 +121,26 @@ Launch a browser to try out your universal login box for the given client.
// Use the access token to fetch user information from the /userinfo
// endpoint.
userInfo, err = auth.FetchUserInfo(tenant.Domain, tokenResponse.AccessToken)
if err != nil {
return err
}

return err
// if we added the local callback URL to the client then we need to
// remove it when we're done
if needsLocalCallbackURL {
if err := removeLocalCallbackURLFromClient(cli.api.Client, client); err != nil {
return err
}
}

return nil
})

if err != nil {
return err
}

fmt.Fprint(cli.renderer.MessageWriter, "\n")
cli.renderer.TryLogin(userInfo, tokenResponse, reveal)
return nil
},
Expand Down Expand Up @@ -161,6 +178,62 @@ func getOrCreateCLITesterClient(clientManager auth0.ClientAPI) (*management.Clie
return client, clientManager.Create(client)
}

// check if a client is already configured with our local callback URL
func checkForLocalCallbackURL(client *management.Client) bool {
for _, rawCallbackURL := range client.Callbacks {
callbackURL := rawCallbackURL.(string)
if callbackURL == cliLoginTestingCallbackURL {
return true
}
}

return false
}

// adds the localhost callback URL to a given client
func addLocalCallbackURLToClient(clientManager auth0.ClientAPI, client *management.Client) error {
for _, rawCallbackURL := range client.Callbacks {
callbackURL := rawCallbackURL.(string)
if callbackURL == cliLoginTestingCallbackURL {
return nil
}
}

updatedClient := &management.Client{
Callbacks: append(client.Callbacks, cliLoginTestingCallbackURL),
}
// reflect the changes in the original client instance so when we check it
// later it has the proper values in Callbacks
client.Callbacks = updatedClient.Callbacks
return clientManager.Update(client.GetClientID(), updatedClient)
}

func removeLocalCallbackURLFromClient(clientManager auth0.ClientAPI, client *management.Client) error {
callbacks := []interface{}{}
for _, rawCallbackURL := range client.Callbacks {
callbackURL := rawCallbackURL.(string)
if callbackURL != cliLoginTestingCallbackURL {
callbacks = append(callbacks, callbackURL)
}
}

// no callback URLs to remove, so don't attempt to do so
if len(client.Callbacks) == len(callbacks) {
return nil
}

// can't update a client to have 0 callback URLs, so don't attempt it
if len(callbacks) == 1 {
return nil
}

updatedClient := &management.Client{
Callbacks: callbacks,
}
return clientManager.Update(client.GetClientID(), updatedClient)

}

// buildInitiateLoginURL constructs a URL + query string that can be used to
// initiate a login-flow from the CLI.
func buildInitiateLoginURL(domain, clientID, connectionName string) (string, error) {
Expand Down
16 changes: 10 additions & 6 deletions internal/prompt/prompt.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package prompt

import (
"os"

"github.com/AlecAivazis/survey/v2"
)

func Ask(inputs []*survey.Question, response interface{}, options ...survey.AskOpt) error {
return survey.Ask(inputs, response, options...)
var stdErrWriter = survey.WithStdio(os.Stdin, os.Stderr, os.Stderr)

func Ask(inputs []*survey.Question, response interface{}) error {
return survey.Ask(inputs, response, stdErrWriter)
}

func TextInput(name string, message string, required bool) *survey.Question {
input := &survey.Question{
Name: name,
Prompt: &survey.Input{Message: message},
Name: name,
Prompt: &survey.Input{Message: message},
Transform: survey.Title,
}

if required {
input.Validate = survey.Required
}
Expand All @@ -28,7 +32,7 @@ func Confirm(message string) bool {
Message: message,
}

if err := survey.AskOne(prompt, &result); err != nil {
if err := survey.AskOne(prompt, &result, stdErrWriter); err != nil {
return false
}

Expand Down