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

Added new login flow. Respect domain flag on auth0 login command. #1038

Merged
merged 22 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1559c78
Added use case to ensure if domain if passed in cli, it's populated t…
duedares-rvj Jul 15, 2024
05f1b5b
Updated login flow, improved validations, added relevant comments.
duedares-rvj Jul 25, 2024
d00612e
minor update to flow
duedares-rvj Jul 25, 2024
e058d75
Added test cases for new login flow
duedares-rvj Jul 26, 2024
eefe58f
Updated auth_test GetDeviceCode func call
duedares-rvj Jul 26, 2024
72ff8c1
Convert if-else clause to switch case
duedares-rvj Jul 29, 2024
3e96dad
Fixed linitng issues
duedares-rvj Jul 29, 2024
dde1069
Bump golang.org/x/net from 0.26.0 to 0.27.0 (#1035)
dependabot[bot] Jul 10, 2024
87230cd
Bump github.com/auth0/go-auth0 from 1.7.0 to 1.8.0 (#1036)
dependabot[bot] Jul 26, 2024
bcd28e1
Bump rexml from 3.2.8 to 3.3.2 in /docs (#1041)
dependabot[bot] Jul 27, 2024
e833603
Bump github.com/lestrrat-go/jwx from 1.2.29 to 1.2.30 (#1042)
dependabot[bot] Jul 29, 2024
5d3e8e3
Update codeowner file with new GitHub team name (#1039)
stevenwong-okta Jul 30, 2024
e8114f0
Converted sensitive value to env vars
duedares-rvj Aug 2, 2024
c686779
Bump github.com/schollz/progressbar/v3 from 3.14.4 to 3.14.5 (#1043)
dependabot[bot] Jul 31, 2024
00ecd53
Removed a positive case
duedares-rvj Aug 2, 2024
0859d7d
Bump rexml from 3.3.2 to 3.3.3 in /docs (#1044)
dependabot[bot] Aug 4, 2024
8ae93a5
Bump github.com/hashicorp/hc-install from 0.7.0 to 0.8.0 (#1045)
dependabot[bot] Aug 4, 2024
00e00ad
Bump golang.org/x/net from 0.26.0 to 0.27.0 (#1035)
dependabot[bot] Jul 10, 2024
8bfcd9f
Bump golang.org/x/net from 0.26.0 to 0.27.0 (#1035)
dependabot[bot] Jul 10, 2024
2c3764c
Bump golang.org/x/net from 0.26.0 to 0.27.0 (#1035)
dependabot[bot] Jul 10, 2024
ddbd3f7
rebased branch with main
duedares-rvj Aug 5, 2024
c4883bd
Merge branch 'main' into respect-domain-flag-for-user-login
duedares-rvj Aug 8, 2024
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
4 changes: 2 additions & 2 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ var RequiredScopes = []string{
// GetDeviceCode kicks-off the device authentication flow by requesting
// a device code from Auth0. The returned state contains the
// URI for the next step of the flow.
func GetDeviceCode(ctx context.Context, httpClient *http.Client, additionalScopes []string) (State, error) {
func GetDeviceCode(ctx context.Context, httpClient *http.Client, additionalScopes []string, domain string) (State, error) {
acwest marked this conversation as resolved.
Show resolved Hide resolved
a := credentials

data := url.Values{
"client_id": []string{a.ClientID},
"scope": []string{strings.Join(append(RequiredScopes, additionalScopes...), " ")},
"audience": []string{a.Audience},
"audience": []string{domain},
}

request, err := http.NewRequestWithContext(
Expand Down
4 changes: 2 additions & 2 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestGetDeviceCode(t *testing.T) {
u := url.URL{Scheme: "https", Host: parsedURL.Host, Path: "/oauth/device/code"}
credentials.DeviceCodeEndpoint = u.String()

state, err := GetDeviceCode(context.Background(), ts.Client(), []string{})
state, err := GetDeviceCode(context.Background(), ts.Client(), []string{}, "")

assert.NoError(t, err)
assert.Equal(t, "device-code-here", state.DeviceCode)
Expand Down Expand Up @@ -180,7 +180,7 @@ func TestGetDeviceCode(t *testing.T) {
u := url.URL{Scheme: "https", Host: parsedURL.Host, Path: "/oauth/device/code"}
credentials.DeviceCodeEndpoint = u.String()

_, err = GetDeviceCode(context.Background(), ts.Client(), []string{})
_, err = GetDeviceCode(context.Background(), ts.Client(), []string{}, "")

assert.EqualError(t, err, testCase.expect)
})
Expand Down
8 changes: 2 additions & 6 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (c *cli) setupWithAuthentication(ctx context.Context) error {
switch err {
case config.ErrTokenMissingRequiredScopes:
c.renderer.Warnf("Required scopes have changed. Please log in to re-authorize the CLI.\n")
tenant, err = RunLoginAsUser(ctx, c, tenant.GetExtraRequestedScopes())
tenant, err = RunLoginAsUser(ctx, c, tenant.GetExtraRequestedScopes(), "")
if err != nil {
return err
}
Expand All @@ -95,7 +95,7 @@ func (c *cli) setupWithAuthentication(ctx context.Context) error {
c.renderer.Warnf("Failed to renew access token: %s", err)
c.renderer.Warnf("Please log in to re-authorize the CLI.\n")

tenant, err = RunLoginAsUser(ctx, c, tenant.GetExtraRequestedScopes())
tenant, err = RunLoginAsUser(ctx, c, tenant.GetExtraRequestedScopes(), "")
if err != nil {
return err
}
Expand Down Expand Up @@ -136,10 +136,6 @@ func canPrompt(cmd *cobra.Command) bool {
return iostream.IsInputTerminal() && iostream.IsOutputTerminal() && !noInput
}

func shouldPrompt(cmd *cobra.Command, flag *Flag) bool {
return canPrompt(cmd) && !flag.IsSet(cmd)
}

func shouldPromptWhenNoLocalFlagsSet(cmd *cobra.Command) bool {
localFlagIsSet := false
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool {
return shouldPromptWhenNoLocalFlagsSet(cmd)
}

return shouldPrompt(cmd, f)
return canPrompt(cmd) && !f.IsSet(cmd)
}

func markFlagRequired(cmd *cobra.Command, f *Flag, isUpdate bool) error {
Expand Down
107 changes: 90 additions & 17 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ type LoginInputs struct {
AdditionalScopes []string
}

func (i *LoginInputs) isLoggingInAsAMachine() bool {
return i.ClientID != "" || i.ClientSecret != "" || i.Domain != ""
}

func (i *LoginInputs) isLoggingInWithAdditionalScopes() bool {
return len(i.AdditionalScopes) > 0
}
Expand All @@ -82,12 +78,65 @@ func loginCmd(cli *cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
var selectedLoginType string
const loginAsUser, loginAsMachine = "As a user", "As a machine"
shouldLoginAsUser, shouldLoginAsMachine := false, false

/*
Based on the initial inputs we'd like to determine if
it's a machine login or a user login
If we successfully determine it, we don't need to prompt the user.

The --no-input flag add strict restriction that we shall not take any further input after
initial command.
Hence, the flow diverges into two based on no-input flag's value.
*/
switch {
case cli.noInput:
switch {
case inputs.Domain != "" && inputs.ClientSecret != "" && inputs.ClientID != "":
// If all three fields are passed, machine login flag is set to true.
shouldLoginAsMachine = true
case inputs.Domain != "" && inputs.ClientSecret == "" && inputs.ClientID == "":
/*
The domain flag is common between Machine and User Login.
If domain is passed without client-id and client-secret,
it can be evaluated that it is a user login flow.
*/
shouldLoginAsUser = true
case inputs.Domain != "" || inputs.ClientSecret != "" || inputs.ClientID != "":
/*
At this point, if AT LEAST one of the three flags are passed but not ALL three,
we return an error since it's a no-input flow and it will need all three params
for successful machine flow.
Note that we already determined it's not a user login flow in the condition above.
*/
return fmt.Errorf("flags client-id, client-secret and domain are required together")
default:
/*
If no flags are passed along with --no-input, it is defaulted to user login flow.
*/
shouldLoginAsUser = true
}
default:
if inputs.ClientSecret != "" || inputs.ClientID != "" {
/*
If all three params are passed, we evaluate it as a Machine Login Flow.
Else required params are prompted for.
*/
shouldLoginAsMachine = true
}
}

// We want to prompt if we don't pass the following flags:
// --no-input, --scopes, --client-id, --client-secret, --domain.
// Because then the prompt is unnecessary as we know the login type.
shouldPrompt := !inputs.isLoggingInAsAMachine() && !cli.noInput && !inputs.isLoggingInWithAdditionalScopes()
if shouldPrompt {
// If additional scopes are passed we mark shouldLoginAsUser flag to be true.
if inputs.isLoggingInWithAdditionalScopes() {
shouldLoginAsUser = true
}

/*
If we are unable to determine if it's a user login or a machine login
based on all the evaluation above, we go on to prompt the user and
determine if it's LoginAsUser or LoginAsMachine
*/
if !shouldLoginAsUser && !shouldLoginAsMachine {
cli.renderer.Output(
fmt.Sprintf(
"%s\n\n%s\n%s\n\n%s\n%s\n%s\n%s\n\n",
Expand All @@ -107,18 +156,16 @@ func loginCmd(cli *cli) *cobra.Command {
"Authenticating as a user is recommended if performing ad-hoc operations or working locally.",
"Alternatively, authenticating as a machine is recommended for automated workflows (ex:CI).",
)
input := prompt.SelectInput("", label, help, []string{loginAsUser, loginAsMachine}, loginAsUser, shouldPrompt)
input := prompt.SelectInput("", label, help, []string{loginAsUser, loginAsMachine}, loginAsUser, true)
if err := prompt.AskOne(input, &selectedLoginType); err != nil {
return handleInputError(err)
}
}

ctx := cmd.Context()

// Allows to skip to user login if either the --no-input or --scopes flag is passed.
shouldLoginAsUser := (cli.noInput && !inputs.isLoggingInAsAMachine()) || inputs.isLoggingInWithAdditionalScopes() || selectedLoginType == loginAsUser
if shouldLoginAsUser {
if _, err := RunLoginAsUser(ctx, cli, inputs.AdditionalScopes); err != nil {
if shouldLoginAsUser || selectedLoginType == loginAsUser {
if _, err := RunLoginAsUser(ctx, cli, inputs.AdditionalScopes, inputs.Domain); err != nil {
return fmt.Errorf("failed to start the authentication process: %w", err)
}
} else {
Expand All @@ -143,8 +190,8 @@ func loginCmd(cli *cli) *cobra.Command {
loginClientID.RegisterString(cmd, &inputs.ClientID, "")
loginClientSecret.RegisterString(cmd, &inputs.ClientSecret, "")
loginAdditionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, []string{})
cmd.MarkFlagsRequiredTogether("client-id", "client-secret", "domain")
cmd.MarkFlagsMutuallyExclusive("client-id", "scopes")
cmd.MarkFlagsMutuallyExclusive("client-secret", "scopes")

cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
_ = cmd.Flags().MarkHidden("tenant")
Expand All @@ -154,10 +201,36 @@ func loginCmd(cli *cli) *cobra.Command {
return cmd
}

func ensureAuth0URL(input string) (string, error) {
if input == "" {
return "https://*.auth0.com/api/v2/", nil
}
input = strings.TrimPrefix(input, "http://")
input = strings.TrimPrefix(input, "https://")
input = strings.TrimSuffix(input, "/api/v2")

// Check if the input ends with auth0.com .
if !strings.HasSuffix(input, "auth0.com") {
return "", fmt.Errorf("not a valid auth0.com domain")
}

// Extract the domain part without any path.
domainParts := strings.Split(input, "/")
domain := domainParts[0]

// Return the formatted URL.
return fmt.Sprintf("https://%s/api/v2/", domain), nil
}

// RunLoginAsUser runs the login flow guiding the user through the process
// by showing the login instructions, opening the browser.
func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string) (config.Tenant, error) {
state, err := auth.GetDeviceCode(ctx, http.DefaultClient, additionalScopes)
func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string, domain string) (config.Tenant, error) {
domain, err := ensureAuth0URL(domain)
if err != nil {
return config.Tenant{}, err
}

state, err := auth.GetDeviceCode(ctx, http.DefaultClient, additionalScopes, domain)
if err != nil {
return config.Tenant{}, fmt.Errorf("failed to get the device code: %w", err)
}
Expand Down
55 changes: 55 additions & 0 deletions internal/cli/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cli

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestLoginCommand(t *testing.T) {
t.Run("Negative Test: it returns an error since client-id, client-secret and domain must be passed together", func(t *testing.T) {
cli := &cli{}
cli.noInput = true
cmd := loginCmd(cli)
cmd.SetArgs([]string{"--client-id", "t3dbMFeTokYBguVu1Ty88gqntUXELSn9"})
err := cmd.Execute()

assert.EqualError(t, err, "flags client-id, client-secret and domain are required together")
})

t.Run("Negative Test: it returns an error since client-id, client-secret and domain must be passed together", func(t *testing.T) {
cli := &cli{}
cli.noInput = true
cmd := loginCmd(cli)
cmd.SetArgs([]string{"--client-secret", "3OAzE7j2HTnGOPeCRFX3Hg-0sipaEnodzQK8xpwsRiTkqdjjwEFT04rgCjfslianfs"})
err := cmd.Execute()
assert.EqualError(t, err, "flags client-id, client-secret and domain are required together")
})

t.Run("Negative Test: it returns an error since client-id, client-secret and domain must be passed together", func(t *testing.T) {
cli := &cli{}
cli.noInput = true
cmd := loginCmd(cli)
cmd.SetArgs([]string{"--client-id", "t3dbMFeTokYBguVu1Ty88gqntUXELSn9", "--client-secret", "3OAzE7j2HTnGOPeCRFX3Hg-0sipaEnodzQK8xpkqdjjwEFT0EFT04rgCp4PZL4Z"})
err := cmd.Execute()
assert.EqualError(t, err, "flags client-id, client-secret and domain are required together")
})

t.Run("Negative Test: it returns an error since client-id, client-secret and domain must be passed together", func(t *testing.T) {
cli := &cli{}
cli.noInput = true
cmd := loginCmd(cli)
cmd.SetArgs([]string{"--client-id", "t3dbMFeTokYBguVu1Ty88gqntUXELSn9", "--domain", "duedares.us.auth0.com"})
err := cmd.Execute()
assert.EqualError(t, err, "flags client-id, client-secret and domain are required together")
})

t.Run("Negative Test: it returns an error since client-id, client-secret and domain must be passed together", func(t *testing.T) {
cli := &cli{}
cli.noInput = true
cmd := loginCmd(cli)
cmd.SetArgs([]string{"--client-secret", "3OAzE7j2HTnGOPeCRFX3Hg-0sipaEnodzQK8xpkqdjjwEFT0EFT04rgCp4PZL4Z", "--domain", "duedares.us.auth0.com"})
err := cmd.Execute()
assert.EqualError(t, err, "flags client-id, client-secret and domain are required together")
})
}
Loading