From 2cb6f19024547f0e2a776bb1f2ea41ed53efdb7a Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 09:34:09 -0500 Subject: [PATCH 01/13] Adding additional scopes support via --scopes flag --- internal/auth/auth.go | 97 ++++++++++++++++++++++------------------ internal/cli/api.go | 37 +++++++++++++++ internal/cli/api_test.go | 77 +++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 44 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 9955fe8e3..75eee6367 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -29,7 +29,7 @@ var requiredScopes = []string{ "offline_access", // This is used to retrieve a refresh token. "create:clients", "read:clients", "update:clients", "delete:clients", "read:client_keys", - "create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants", + // "create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants", "create:resource_servers", "read:resource_servers", "update:resource_servers", "delete:resource_servers", "create:connections", "read:connections", "update:connections", "delete:connections", "create:users", "read:users", "update:users", "delete:users", @@ -136,17 +136,14 @@ func New() (*Authenticator, error) { return &authenticator, nil } -// Start 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 (a *Authenticator) Start(ctx context.Context) (State, error) { - state, err := a.getDeviceCode(ctx) - if err != nil { - return State{}, fmt.Errorf("failed to get the device code: %w", err) - } +// func (a *Authenticator) Start(ctx context.Context, additionalScopes []string) (State, error) { +// state, err := a.getDeviceCode(ctx) +// if err != nil { +// return State{}, fmt.Errorf("failed to get the device code: %w", err) +// } - return state, nil -} +// return state, nil +// } // Wait waits until the user is logged in on the browser. func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { @@ -208,49 +205,61 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { } } -func (a *Authenticator) getDeviceCode(ctx context.Context) (State, error) { - data := url.Values{ - "client_id": []string{a.ClientID}, - "scope": []string{strings.Join(requiredScopes, " ")}, - "audience": []string{a.Audience}, - } +// 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 (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []string) (State, error) { + state, err := func() (State, error) { - request, err := http.NewRequestWithContext( - ctx, - http.MethodPost, - a.DeviceCodeEndpoint, - strings.NewReader(data.Encode()), - ) - if err != nil { - return State{}, fmt.Errorf("failed to create the request: %w", err) - } + scopesToRequest := append(requiredScopes, additionalScopes...) - request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + data := url.Values{ + "client_id": []string{a.ClientID}, + "scope": []string{strings.Join(scopesToRequest, " ")}, + "audience": []string{a.Audience}, + } - response, err := http.DefaultClient.Do(request) - if err != nil { - return State{}, fmt.Errorf("failed to send the request: %w", err) - } - defer response.Body.Close() + request, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + a.DeviceCodeEndpoint, + strings.NewReader(data.Encode()), + ) + if err != nil { + return State{}, fmt.Errorf("failed to create the request: %w, ", err) + } + + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest { - bodyBytes, err := io.ReadAll(response.Body) + response, err := http.DefaultClient.Do(request) if err != nil { - return State{}, fmt.Errorf( - "received a %d response and failed to read the response", - response.StatusCode, - ) + return State{}, fmt.Errorf("failed to send the request: %w", err) } + defer response.Body.Close() - return State{}, fmt.Errorf("received a %d response: %s", response.StatusCode, bodyBytes) - } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest { + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + return State{}, fmt.Errorf( + "received a %d response and failed to read the response", + response.StatusCode, + ) + } - var state State - err = json.NewDecoder(response.Body).Decode(&state) + return State{}, fmt.Errorf("received a %d response: %s", response.StatusCode, bodyBytes) + } + + var state State + err = json.NewDecoder(response.Body).Decode(&state) + if err != nil { + return State{}, fmt.Errorf("failed to decode the response: %w", err) + } + + return state, nil + }() if err != nil { - return State{}, fmt.Errorf("failed to decode the response: %w", err) + fmt.Errorf("failed to get the device code: %w", err) } - return state, nil } diff --git a/internal/cli/api.go b/internal/cli/api.go index 3b466f2fe..f9672aee1 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -141,6 +141,16 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s } response, err = http.DefaultClient.Do(request) + + doesLackScopes, whichScope := isInsufficientScopeError(response) + + if doesLackScopes { + cli.renderer.Errorf("request failed because access token lacks the %s scope. If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, include this scope during login by running `auth0 login --scopes %s`.", whichScope) + return err + } + + cli.renderer.Infof("%+v", response, response.StatusCode) + return err }); err != nil { return fmt.Errorf("failed to send request: %w", err) @@ -257,3 +267,30 @@ func (i *apiCmdInputs) parseRaw(args []string) { i.RawURI = args[lenArgs-1] } + +func isInsufficientScopeError(r *http.Response) (bool, string) { + + if r.StatusCode != 403 { + return false, "" + } + + type ErrorBody struct { + ErrorCode string `json:"errorCode"` + Message string `json:"message"` + } + + var body ErrorBody + err := json.NewDecoder(r.Body).Decode(&body) + if err != nil { + return false, "" + } + + if body.ErrorCode != "insufficient_scope" { + return false, "" + } + + missingScopes := strings.Split(body.Message, "Insufficient scope, expected any of: ")[1] + recommendedScopesToAdd := strings.Split(missingScopes, ",") + + return true, recommendedScopesToAdd[0] +} diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index 581ff60ca..980554461 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -1,6 +1,8 @@ package cli import ( + "bytes" + "io/ioutil" "net/http" "testing" @@ -85,3 +87,78 @@ func TestAPICmdInputs_FromArgs(t *testing.T) { }) } } + +func TestAPICmd_IsInsufficientScopeError(t *testing.T) { + var testCases = []struct { + name string + inputStatusCode int + inputResponseBody string + expectedResult bool + expectedScope string + }{ + { + name: "it does not detect 404 error", + inputStatusCode: 404, + inputResponseBody: `{ + "statusCode": 404, + "error": "Not Found", + "message": "Not Found" + }`, + expectedResult: false, + expectedScope: "", + }, + { + name: "it does not detect a 200 HTTP response", + inputStatusCode: 200, + inputResponseBody: `{ + "allowed_logout_urls": [], + "change_password": { + "enabled": true, + "html": "LOL" + }, + "default_audience": "", + }`, + expectedResult: false, + expectedScope: "", + }, + { + name: "it correctly detects an insufficient scope error", + inputStatusCode: 403, + inputResponseBody: `{ + "statusCode": 403, + "error": "Forbidden", + "message": "Insufficient scope, expected any of: create:client_grants", + "errorCode": "insufficient_scope" + }`, + expectedResult: true, + expectedScope: "create:client_grants", + }, + { + name: "it correctly detects an insufficient scope error with multiple scope suggestions", + inputStatusCode: 403, + inputResponseBody: `{ + "statusCode": 403, + "error": "Forbidden", + "message": "Insufficient scope, expected any of: read:clients, read:client_summary", + "errorCode": "insufficient_scope" + }`, + expectedResult: true, + expectedScope: "read:clients", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + input := http.Response{ + Body: ioutil.NopCloser(bytes.NewReader([]byte(testCase.inputResponseBody))), + StatusCode: testCase.inputStatusCode, + } + + actualRespBool, actualScope := isInsufficientScopeError(input) + + assert.Equal(t, testCase.expectedResult, actualRespBool) + assert.Equal(t, testCase.expectedScope, actualScope) + }) + } +} From 06723ecee10a675065cd39992fdc6ae7b774a473 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 09:42:28 -0500 Subject: [PATCH 02/13] Adding additional scopes support via --scopes flag --- internal/cli/api_test.go | 2 +- internal/cli/cli.go | 4 ++-- internal/cli/login.go | 40 +++++++++++++++++++++++++++++----------- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index 980554461..ec7d34b5f 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -134,7 +134,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { expectedScope: "create:client_grants", }, { - name: "it correctly detects an insufficient scope error with multiple scope suggestions", + name: "it correctly detects an insufficient scope error with multiple scope", inputStatusCode: 403, inputResponseBody: `{ "statusCode": 403, diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 8a5be713d..cbe51b205 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -213,7 +213,7 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { if scopesChanged(t) && t.authenticatedWithDeviceCodeFlow() { c.renderer.Warnf("Required scopes have changed. Please log in to re-authorize the CLI.\n") - return RunLoginAsUser(ctx, c) + return RunLoginAsUser(ctx, c, []string{}) } if t.AccessToken != "" && !t.hasExpiredToken() { @@ -232,7 +232,7 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { c.renderer.Warnf("Failed to renew access token. Please log in to re-authorize the CLI.\n") - return RunLoginAsUser(ctx, c) + return RunLoginAsUser(ctx, c, []string{}) } if err := c.addTenant(t); err != nil { diff --git a/internal/cli/login.go b/internal/cli/login.go index 950cc1a72..f6606a274 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -36,18 +36,31 @@ var ( IsRequired: false, AlwaysPrompt: false, } + + additionalScopes = Flag{ + Name: "Additional Scopes", + LongForm: "scopes", + Help: "Scopes to request in addition to required defaults when authenticating via device code flow. Primarily useful when using `api` command to execute arbitrary Management API requests.", + IsRequired: false, + AlwaysPrompt: false, + } ) type LoginInputs struct { - Domain string - ClientID string - ClientSecret string + Domain string + ClientID string + ClientSecret string + AdditionalScopes []string } func (i *LoginInputs) isLoggingInAsAMachine() bool { return i.ClientID != "" || i.ClientSecret != "" || i.Domain != "" } +func (i *LoginInputs) isLoggingInWithAdditionalScopes() bool { + return len(i.AdditionalScopes) > 0 +} + func loginCmd(cli *cli) *cobra.Command { var inputs LoginInputs @@ -57,15 +70,18 @@ func loginCmd(cli *cli) *cobra.Command { Short: "Authenticate the Auth0 CLI", Long: "Authenticates the Auth0 CLI either as a user using personal credentials or as a machine using client credentials.", Example: `auth0 login -auth0 login --domain --client-id --client-secret `, +auth0 login --domain --client-id --client-secret +auth0 login --scopes "read:client_grants,create:client_grants"`, RunE: func(cmd *cobra.Command, args []string) error { var selectedLoginType string const loginAsUser, loginAsMachine = "As a user", "As a machine" + cli.renderer.Infof("%+v", inputs.AdditionalScopes, len(inputs.AdditionalScopes)) + // We want to prompt if we don't pass the following flags: - // --no-input, --client-id, --client-secret, --domain. + // --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 + shouldPrompt := !inputs.isLoggingInAsAMachine() && !cli.noInput && !inputs.isLoggingInWithAdditionalScopes() if shouldPrompt { cli.renderer.Output( fmt.Sprintf( @@ -94,10 +110,10 @@ auth0 login --domain --client-id --client-secret --client-id --client-secret --client-id --client-secret Date: Thu, 15 Dec 2022 09:59:55 -0500 Subject: [PATCH 03/13] Removing logging --- internal/cli/api.go | 5 ++--- internal/cli/api_test.go | 2 +- internal/cli/login.go | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index f9672aee1..b441871fa 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -145,12 +145,11 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s doesLackScopes, whichScope := isInsufficientScopeError(response) if doesLackScopes { - cli.renderer.Errorf("request failed because access token lacks the %s scope. If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, include this scope during login by running `auth0 login --scopes %s`.", whichScope) + cli.renderer.Errorf("request failed because access token lacks scope: %s.", whichScope) + cli.renderer.Errorf("If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.", whichScope) return err } - cli.renderer.Infof("%+v", response, response.StatusCode) - return err }); err != nil { return fmt.Errorf("failed to send request: %w", err) diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index ec7d34b5f..ccf6eac1a 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -155,7 +155,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { StatusCode: testCase.inputStatusCode, } - actualRespBool, actualScope := isInsufficientScopeError(input) + actualRespBool, actualScope := isInsufficientScopeError(&input) assert.Equal(t, testCase.expectedResult, actualRespBool) assert.Equal(t, testCase.expectedScope, actualScope) diff --git a/internal/cli/login.go b/internal/cli/login.go index f6606a274..a1b467a81 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -76,8 +76,6 @@ auth0 login --scopes "read:client_grants,create:client_grants"`, var selectedLoginType string const loginAsUser, loginAsMachine = "As a user", "As a machine" - cli.renderer.Infof("%+v", inputs.AdditionalScopes, len(inputs.AdditionalScopes)) - // 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. From d55fd0d329fd1a28c13d5a8b53a40a43ac73b5ca Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 10:03:25 -0500 Subject: [PATCH 04/13] Uncommenting scope, removing Start function --- internal/auth/auth.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 75eee6367..b5c2642d7 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -29,7 +29,7 @@ var requiredScopes = []string{ "offline_access", // This is used to retrieve a refresh token. "create:clients", "read:clients", "update:clients", "delete:clients", "read:client_keys", - // "create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants", + "create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants", "create:resource_servers", "read:resource_servers", "update:resource_servers", "delete:resource_servers", "create:connections", "read:connections", "update:connections", "delete:connections", "create:users", "read:users", "update:users", "delete:users", @@ -136,15 +136,6 @@ func New() (*Authenticator, error) { return &authenticator, nil } -// func (a *Authenticator) Start(ctx context.Context, additionalScopes []string) (State, error) { -// state, err := a.getDeviceCode(ctx) -// if err != nil { -// return State{}, fmt.Errorf("failed to get the device code: %w", err) -// } - -// return state, nil -// } - // Wait waits until the user is logged in on the browser. func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { t := time.NewTicker(state.IntervalDuration()) From b96c834a7490d642b1d29fcf24f96159c752775f Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 11:51:15 -0500 Subject: [PATCH 05/13] Condensing error to single line --- internal/auth/auth.go | 59 ++++++++++++------------------------------- internal/cli/api.go | 3 +-- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b5c2642d7..d4aee8572 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -26,50 +26,23 @@ const ( var requiredScopes = []string{ "openid", - "offline_access", // This is used to retrieve a refresh token. - "create:clients", "read:clients", "update:clients", "delete:clients", - "read:client_keys", - "create:client_grants", "read:client_grants", "update:client_grants", "delete:client_grants", - "create:resource_servers", "read:resource_servers", "update:resource_servers", "delete:resource_servers", - "create:connections", "read:connections", "update:connections", "delete:connections", - "create:users", "read:users", "update:users", "delete:users", - "create:roles", "read:roles", "update:roles", "delete:roles", - "create:actions", "read:actions", "update:actions", "delete:actions", - "read:triggers", "update:triggers", - "create:rules", "read:rules", "update:rules", "delete:rules", - "read:rules_configs", "update:rules_configs", "delete:rules_configs", - "create:hooks", "read:hooks", "update:hooks", "delete:hooks", - "read:attack_protection", "update:attack_protection", - "create:organizations", "read:organizations", "update:organizations", "delete:organizations", - "create:organization_members", "read:organization_members", "delete:organization_members", - "create:organization_connections", "read:organization_connections", "update:organization_connections", "delete:organization_connections", - "create:organization_member_roles", "read:organization_member_roles", "delete:organization_member_roles", - "create:organization_invitations", "read:organization_invitations", "delete:organization_invitations", - "read:prompts", "update:prompts", - "read:branding", "update:branding", "delete:branding", - "create:custom_domains", "read:custom_domains", "update:custom_domains", "delete:custom_domains", - "create:email_provider", "read:email_provider", "update:email_provider", "delete:email_provider", - "create:email_templates", "read:email_templates", "update:email_templates", - "read:tenant_settings", "update:tenant_settings", + "offline_access", // for retrieving refresh token + "create:clients", "delete:clients", "read:clients", "update:clients", + "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", + "create:roles", "delete:roles", "read:roles", "update:roles", + "create:rules", "delete:rules", "read:rules", "update:rules", + "create:users", "delete:users", "read:users", "update:users", + "read:branding", "update:branding", + "read:email_templates", "update:email_templates", + "read:connections", "update:connections", + "read:client_keys", "read:logs", "read:tenant_settings", + "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", "read:anomaly_blocks", "delete:anomaly_blocks", - "create:log_streams", "read:log_streams", "update:log_streams", "delete:log_streams", - "read:stats", - "read:insights", - "read:logs", - "create:shields", "read:shields", "update:shields", "delete:shields", - "create:users_app_metadata", "read:users_app_metadata", "update:users_app_metadata", "delete:users_app_metadata", - "create:user_custom_blocks", "read:user_custom_blocks", "delete:user_custom_blocks", - "create:user_tickets", - "blacklist:tokens", - "read:grants", "delete:grants", - "read:mfa_policies", "update:mfa_policies", - "read:guardian_factors", "update:guardian_factors", - "read:guardian_enrollments", "delete:guardian_enrollments", - "create:guardian_enrollment_tickets", - "read:user_idp_tokens", - "create:passwords_checking_job", "delete:passwords_checking_job", - "read:limits", "update:limits", - "read:entitlements", + "create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams", + "create:actions", "delete:actions", "read:actions", "update:actions", + "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", + "read:prompts", "update:prompts", + "read:attack_protection", "update:attack_protection", } // Authenticator is used to facilitate the login process. diff --git a/internal/cli/api.go b/internal/cli/api.go index b441871fa..eb64aeace 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -145,8 +145,7 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s doesLackScopes, whichScope := isInsufficientScopeError(response) if doesLackScopes { - cli.renderer.Errorf("request failed because access token lacks scope: %s.", whichScope) - cli.renderer.Errorf("If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.", whichScope) + cli.renderer.Errorf("request failed because access token lacks scope: %s.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.", whichScope, whichScope) return err } From 607a197625d97917c1a1644080544546343599c4 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 13:32:28 -0500 Subject: [PATCH 06/13] Fixing linting errors --- go.sum | 14 -------------- internal/auth/auth.go | 13 +++++-------- internal/cli/api.go | 1 - internal/cli/api_test.go | 5 ++--- internal/cli/login.go | 2 +- 5 files changed, 8 insertions(+), 27 deletions(-) diff --git a/go.sum b/go.sum index 7d31f5f50..ba69984c2 100644 --- a/go.sum +++ b/go.sum @@ -206,21 +206,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.3.0 h1:VWL6FNY2bEEmsGVKabSlHu5Irp34xmMRoqb/9lF9lxk= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -248,7 +235,6 @@ golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 9256f8afb..713e3865b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -96,7 +96,7 @@ func (s *State) IntervalDuration() time.Duration { return time.Duration(s.Interval+waitThresholdInSeconds) * time.Second } -// New returns a new instance of Authenticator using Auth0 Public Cloud default values +// New returns a new instance of Authenticator using Auth0 Public Cloud default values. func New() *Authenticator { return &Authenticator{ Audience: "https://*.auth0.com/api/v2/", @@ -171,12 +171,9 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { // URI for the next step of the flow. func (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []string) (State, error) { state, err := func() (State, error) { - - scopesToRequest := append(requiredScopes, additionalScopes...) - data := url.Values{ "client_id": []string{a.ClientID}, - "scope": []string{strings.Join(scopesToRequest, " ")}, + "scope": []string{strings.Join(append(requiredScopes, additionalScopes...), " ")}, "audience": []string{a.Audience}, } @@ -219,7 +216,7 @@ func (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []st return state, nil }() if err != nil { - fmt.Errorf("failed to get the device code: %w", err) + return State{}, fmt.Errorf("failed to get the device code: %w", err) } return state, nil } @@ -250,14 +247,14 @@ func parseTenant(accessToken string) (tenant, domain string, err error) { return "", "", fmt.Errorf("audience not found for %s", audiencePath) } -// ClientCredentials encapsulates all data to facilitate access token creation with client credentials (client ID and client secret) +// ClientCredentials encapsulates all data to facilitate access token creation with client credentials (client ID and client secret). type ClientCredentials struct { ClientID string ClientSecret string Domain string } -// GetAccessTokenFromClientCreds generates an access token from client credentials +// GetAccessTokenFromClientCreds generates an access token from client credentials. func GetAccessTokenFromClientCreds(ctx context.Context, args ClientCredentials) (Result, error) { u, err := url.Parse("https://" + args.Domain) if err != nil { diff --git a/internal/cli/api.go b/internal/cli/api.go index eb64aeace..fe2aa40f9 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -267,7 +267,6 @@ func (i *apiCmdInputs) parseRaw(args []string) { } func isInsufficientScopeError(r *http.Response) (bool, string) { - if r.StatusCode != 403 { return false, "" } diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index ccf6eac1a..c7cfffd94 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -2,7 +2,7 @@ package cli import ( "bytes" - "io/ioutil" + "io" "net/http" "testing" @@ -149,9 +149,8 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - input := http.Response{ - Body: ioutil.NopCloser(bytes.NewReader([]byte(testCase.inputResponseBody))), + Body: io.NopCloser(bytes.NewReader([]byte(testCase.inputResponseBody))), StatusCode: testCase.inputStatusCode, } diff --git a/internal/cli/login.go b/internal/cli/login.go index a1b467a81..154c2d78b 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -233,7 +233,7 @@ func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string) (T return tenant, nil } -// RunLoginAsMachine facilitates the authentication process using client credentials (client ID, client secret) +// 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 From 7d58fa9cecce5cea771200aa8b41c3f244a244f4 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 14:23:12 -0500 Subject: [PATCH 07/13] Changing test --- internal/auth/auth_test.go | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 0d360d326..cb73cebc0 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -6,21 +6,15 @@ func TestRequiredScopes(t *testing.T) { t.Run("Verify CRUD scopes", func(t *testing.T) { crudResources := []string{ "clients", - "client_grants", - "connections", "log_streams", "resource_servers", "roles", "rules", "users", + "custom_domains", + "log_streams", "actions", - "hooks", "organizations", - "organization_connections", - "custom_domains", - "email_provider", - "shields", - "users_app_metadata", } crudPrefixes := []string{"create:", "delete:", "read:", "update:"} @@ -37,27 +31,15 @@ func TestRequiredScopes(t *testing.T) { t.Run("Verify special scopes", func(t *testing.T) { list := []string{ - "read:branding", "update:branding", "delete:branding", - "read:triggers", "update:triggers", - "read:client_keys", - "read:logs", - "read:tenant_settings", "update:tenant_settings", + "read:branding", "update:branding", + "read:connections", "update:connections", + "read:email_templates", "update:email_templates", + "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", + "read:client_keys", "read:logs", "read:tenant_settings", "read:anomaly_blocks", "delete:anomaly_blocks", - "read:attack_protection", "update:attack_protection", + "read:organization_members", "read:organization_member_roles", "read:prompts", "update:prompts", - "read:stats", - "read:insights", - "create:user_tickets", - "blacklist:tokens", - "read:grants", "delete:grants", - "read:mfa_policies", "update:mfa_policies", - "read:guardian_factors", "update:guardian_factors", - "read:guardian_enrollments", "delete:guardian_enrollments", - "create:guardian_enrollment_tickets", - "read:user_idp_tokens", - "create:passwords_checking_job", "delete:passwords_checking_job", - "read:limits", "update:limits", - "read:entitlements", + "read:attack_protection", "update:attack_protection", } for _, v := range list { From a2f58603daa6378568c3fb292ea6c33bf768d076 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 14:25:39 -0500 Subject: [PATCH 08/13] Updating docs --- docs/auth0_login.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/auth0_login.md b/docs/auth0_login.md index e22cb0f2e..89419a2ce 100644 --- a/docs/auth0_login.md +++ b/docs/auth0_login.md @@ -18,6 +18,7 @@ auth0 login [flags] ``` auth0 login auth0 login --domain --client-id --client-secret +auth0 login --scopes "read:client_grants,create:client_grants" ``` ### Options @@ -27,6 +28,7 @@ auth0 login --domain --client-id --client-secret Date: Thu, 15 Dec 2022 14:40:47 -0500 Subject: [PATCH 09/13] Unpluralizing text, setting nil default value --- internal/cli/api.go | 4 ++-- internal/cli/login.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/cli/api.go b/internal/cli/api.go index fe2aa40f9..90913bca0 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -287,7 +287,7 @@ func isInsufficientScopeError(r *http.Response) (bool, string) { } missingScopes := strings.Split(body.Message, "Insufficient scope, expected any of: ")[1] - recommendedScopesToAdd := strings.Split(missingScopes, ",") + recommendedScopeToAdd := strings.Split(missingScopes, ",") - return true, recommendedScopesToAdd[0] + return true, recommendedScopeToAdd[0] } diff --git a/internal/cli/login.go b/internal/cli/login.go index 154c2d78b..95e180294 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -129,7 +129,7 @@ auth0 login --scopes "read:client_grants,create:client_grants"`, loginTenantDomain.RegisterString(cmd, &inputs.Domain, "") loginClientID.RegisterString(cmd, &inputs.ClientID, "") loginClientSecret.RegisterString(cmd, &inputs.ClientSecret, "") - additionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, []string{}) + additionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, nil) cmd.MarkFlagsRequiredTogether("client-id", "client-secret", "domain") cmd.MarkFlagsMutuallyExclusive("client-id", "scopes") From d5caf06223946d92c241c98d0928ec9b55beb9e9 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Thu, 15 Dec 2022 15:02:33 -0500 Subject: [PATCH 10/13] Fixing bad help text --- docs/auth0_login.md | 2 +- internal/cli/login.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/auth0_login.md b/docs/auth0_login.md index 89419a2ce..dd1ae31a4 100644 --- a/docs/auth0_login.md +++ b/docs/auth0_login.md @@ -28,7 +28,7 @@ auth0 login --scopes "read:client_grants,create:client_grants" --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. -h, --help help for login - --scopes api Scopes to request in addition to required defaults when authenticating via device code flow. Primarily useful when using api command to execute arbitrary Management API requests. + --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. ``` ### Options inherited from parent commands diff --git a/internal/cli/login.go b/internal/cli/login.go index 95e180294..0300c243d 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -37,10 +37,10 @@ var ( AlwaysPrompt: false, } - additionalScopes = Flag{ + loginAdditionalScopes = Flag{ Name: "Additional Scopes", LongForm: "scopes", - Help: "Scopes to request in addition to required defaults when authenticating via device code flow. Primarily useful when using `api` command to execute arbitrary Management API requests.", + Help: "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.", IsRequired: false, AlwaysPrompt: false, } @@ -129,7 +129,7 @@ auth0 login --scopes "read:client_grants,create:client_grants"`, loginTenantDomain.RegisterString(cmd, &inputs.Domain, "") loginClientID.RegisterString(cmd, &inputs.ClientID, "") loginClientSecret.RegisterString(cmd, &inputs.ClientSecret, "") - additionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, nil) + loginAdditionalScopes.RegisterStringSlice(cmd, &inputs.AdditionalScopes, []string{}) cmd.MarkFlagsRequiredTogether("client-id", "client-secret", "domain") cmd.MarkFlagsMutuallyExclusive("client-id", "scopes") From e23b9b21a509eeccd644ef22a6e89811081a24e1 Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea Date: Fri, 16 Dec 2022 18:12:54 +0100 Subject: [PATCH 11/13] Tiny refactors on the login cmd --- internal/auth/auth.go | 73 ++++++++++++++++++---------------------- internal/cli/api.go | 33 +++++++++--------- internal/cli/api_test.go | 25 ++++++-------- internal/cli/cli.go | 54 ++++++++++++++++------------- internal/cli/cli_test.go | 27 +++++++++++++++ internal/cli/login.go | 6 ++-- 6 files changed, 121 insertions(+), 97 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 713e3865b..093a8bcfe 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -170,54 +170,47 @@ func (a *Authenticator) Wait(ctx context.Context, state State) (Result, error) { // a device code from Auth0. The returned state contains the // URI for the next step of the flow. func (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []string) (State, error) { - state, err := func() (State, error) { - data := url.Values{ - "client_id": []string{a.ClientID}, - "scope": []string{strings.Join(append(requiredScopes, additionalScopes...), " ")}, - "audience": []string{a.Audience}, - } - - request, err := http.NewRequestWithContext( - ctx, - http.MethodPost, - a.DeviceCodeEndpoint, - strings.NewReader(data.Encode()), - ) - if err != nil { - return State{}, fmt.Errorf("failed to create the request: %w, ", err) - } + data := url.Values{ + "client_id": []string{a.ClientID}, + "scope": []string{strings.Join(append(requiredScopes, additionalScopes...), " ")}, + "audience": []string{a.Audience}, + } - request.Header.Set("Content-Type", "application/x-www-form-urlencoded") + request, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + a.DeviceCodeEndpoint, + strings.NewReader(data.Encode()), + ) + if err != nil { + return State{}, fmt.Errorf("failed to create the request: %w, ", err) + } - response, err := http.DefaultClient.Do(request) - if err != nil { - return State{}, fmt.Errorf("failed to send the request: %w", err) - } - defer response.Body.Close() + request.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusBadRequest { - bodyBytes, err := io.ReadAll(response.Body) - if err != nil { - return State{}, fmt.Errorf( - "received a %d response and failed to read the response", - response.StatusCode, - ) - } - - return State{}, fmt.Errorf("received a %d response: %s", response.StatusCode, bodyBytes) - } + response, err := http.DefaultClient.Do(request) + if err != nil { + return State{}, fmt.Errorf("failed to send the request: %w", err) + } + defer response.Body.Close() - var state State - err = json.NewDecoder(response.Body).Decode(&state) + if response.StatusCode != http.StatusOK { + bodyBytes, err := io.ReadAll(response.Body) if err != nil { - return State{}, fmt.Errorf("failed to decode the response: %w", err) + return State{}, fmt.Errorf( + "received a %d response and failed to read the response", + response.StatusCode, + ) } - return state, nil - }() - if err != nil { - return State{}, fmt.Errorf("failed to get the device code: %w", err) + return State{}, fmt.Errorf("received a %d response: %s", response.StatusCode, bodyBytes) + } + + var state State + if err = json.NewDecoder(response.Body).Decode(&state); err != nil { + return State{}, fmt.Errorf("failed to decode the response: %w", err) } + return state, nil } diff --git a/internal/cli/api.go b/internal/cli/api.go index 90913bca0..960cc59ab 100644 --- a/internal/cli/api.go +++ b/internal/cli/api.go @@ -141,20 +141,16 @@ func apiCmdRun(cli *cli, inputs *apiCmdInputs) func(cmd *cobra.Command, args []s } response, err = http.DefaultClient.Do(request) - - doesLackScopes, whichScope := isInsufficientScopeError(response) - - if doesLackScopes { - cli.renderer.Errorf("request failed because access token lacks scope: %s.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.", whichScope, whichScope) - return err - } - return err }); err != nil { return fmt.Errorf("failed to send request: %w", err) } defer response.Body.Close() + if err := isInsufficientScopeError(response); err != nil { + return err + } + rawBodyJSON, err := io.ReadAll(response.Body) if err != nil { return err @@ -266,9 +262,9 @@ func (i *apiCmdInputs) parseRaw(args []string) { i.RawURI = args[lenArgs-1] } -func isInsufficientScopeError(r *http.Response) (bool, string) { +func isInsufficientScopeError(r *http.Response) error { if r.StatusCode != 403 { - return false, "" + return nil } type ErrorBody struct { @@ -277,17 +273,22 @@ func isInsufficientScopeError(r *http.Response) (bool, string) { } var body ErrorBody - err := json.NewDecoder(r.Body).Decode(&body) - if err != nil { - return false, "" + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + return nil } if body.ErrorCode != "insufficient_scope" { - return false, "" + return nil } missingScopes := strings.Split(body.Message, "Insufficient scope, expected any of: ")[1] - recommendedScopeToAdd := strings.Split(missingScopes, ",") + recommendedScopeToAdd := strings.Split(missingScopes, ",")[0] - return true, recommendedScopeToAdd[0] + return fmt.Errorf( + "request failed because access token lacks scope: %s.\n "+ + "If authenticated via client credentials, add this scope to the designated client. "+ + "If authenticated as a user, request this scope during login by running `auth0 login --scopes %s`.", + recommendedScopeToAdd, + recommendedScopeToAdd, + ) } diff --git a/internal/cli/api_test.go b/internal/cli/api_test.go index c7cfffd94..52f92bd81 100644 --- a/internal/cli/api_test.go +++ b/internal/cli/api_test.go @@ -93,8 +93,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { name string inputStatusCode int inputResponseBody string - expectedResult bool - expectedScope string + expectedError string }{ { name: "it does not detect 404 error", @@ -104,8 +103,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { "error": "Not Found", "message": "Not Found" }`, - expectedResult: false, - expectedScope: "", + expectedError: "", }, { name: "it does not detect a 200 HTTP response", @@ -118,8 +116,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { }, "default_audience": "", }`, - expectedResult: false, - expectedScope: "", + expectedError: "", }, { name: "it correctly detects an insufficient scope error", @@ -130,8 +127,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { "message": "Insufficient scope, expected any of: create:client_grants", "errorCode": "insufficient_scope" }`, - expectedResult: true, - expectedScope: "create:client_grants", + expectedError: "request failed because access token lacks scope: create:client_grants.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes create:client_grants`.", }, { name: "it correctly detects an insufficient scope error with multiple scope", @@ -142,8 +138,7 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { "message": "Insufficient scope, expected any of: read:clients, read:client_summary", "errorCode": "insufficient_scope" }`, - expectedResult: true, - expectedScope: "read:clients", + expectedError: "request failed because access token lacks scope: read:clients.\n If authenticated via client credentials, add this scope to the designated client. If authenticated as a user, request this scope during login by running `auth0 login --scopes read:clients`.", }, } @@ -154,10 +149,12 @@ func TestAPICmd_IsInsufficientScopeError(t *testing.T) { StatusCode: testCase.inputStatusCode, } - actualRespBool, actualScope := isInsufficientScopeError(&input) - - assert.Equal(t, testCase.expectedResult, actualRespBool) - assert.Equal(t, testCase.expectedScope, actualScope) + err := isInsufficientScopeError(&input) + if testCase.expectedError == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, testCase.expectedError) + } }) } } diff --git a/internal/cli/cli.go b/internal/cli/cli.go index cbe51b205..00870a0fc 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -9,7 +9,6 @@ import ( "os" "path" "path/filepath" - "sort" "strings" "sync" "time" @@ -108,6 +107,27 @@ func (t *Tenant) hasExpiredToken() bool { return time.Now().Add(accessTokenExpThreshold).After(t.ExpiresAt) } +func (t *Tenant) additionalRequestedScopes() []string { + additionallyRequestedScopes := make([]string, 0) + + for _, scope := range t.Scopes { + found := false + + for _, defaultScope := range auth.RequiredScopes() { + if scope == defaultScope { + found = true + break + } + } + + if !found { + additionallyRequestedScopes = append(additionallyRequestedScopes, scope) + } + } + + return additionallyRequestedScopes +} + func (t *Tenant) regenerateAccessToken(ctx context.Context, c *cli) error { if t.authenticatedWithClientCredentials() { token, err := auth.GetAccessTokenFromClientCreds( @@ -211,9 +231,9 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { return Tenant{}, err } - if scopesChanged(t) && t.authenticatedWithDeviceCodeFlow() { + if !hasAllRequiredScopes(t) && t.authenticatedWithDeviceCodeFlow() { c.renderer.Warnf("Required scopes have changed. Please log in to re-authorize the CLI.\n") - return RunLoginAsUser(ctx, c, []string{}) + return RunLoginAsUser(ctx, c, t.additionalRequestedScopes()) } if t.AccessToken != "" && !t.hasExpiredToken() { @@ -232,7 +252,7 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { c.renderer.Warnf("Failed to renew access token. Please log in to re-authorize the CLI.\n") - return RunLoginAsUser(ctx, c, []string{}) + return RunLoginAsUser(ctx, c, t.additionalRequestedScopes()) } if err := c.addTenant(t); err != nil { @@ -242,30 +262,16 @@ func (c *cli) prepareTenant(ctx context.Context) (Tenant, error) { return t, nil } -// scopesChanged compare the tenant scopes +// hasAllRequiredScopes compare the tenant scopes // with the currently required scopes. -func scopesChanged(t Tenant) bool { - want := auth.RequiredScopes() - got := t.Scopes - - sort.Strings(want) - sort.Strings(got) - - if (want == nil) != (got == nil) { - return true - } - - if len(want) != len(got) { - return true - } - - for i := range t.Scopes { - if want[i] != got[i] { - return true +func hasAllRequiredScopes(t Tenant) bool { + for _, requiredScope := range auth.RequiredScopes() { + if !containsStr(t.Scopes, requiredScope) { + return false } } - return false + return true } // getTenant fetches the default tenant configured (or the tenant specified via diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index e8bbae59c..3fb0adafe 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -13,6 +13,7 @@ import ( "github.com/olekukonko/tablewriter" "github.com/stretchr/testify/assert" + "github.com/auth0/auth0-cli/internal/auth" "github.com/auth0/auth0-cli/internal/display" ) @@ -119,3 +120,29 @@ func TestIsLoggedIn(t *testing.T) { }) } } + +func TestTenant_AdditionalRequestedScopes(t *testing.T) { + var testCases = []struct { + name string + givenScopes []string + expectedScopes []string + }{ + { + name: "it can correctly distinguish additionally requested scopes", + givenScopes: append(auth.RequiredScopes(), "read:stats", "read:client_grants"), + expectedScopes: []string{"read:stats", "read:client_grants"}, + }, + { + name: "it returns an empty string slice if no additional requested scopes were given", + givenScopes: append(auth.RequiredScopes()), + expectedScopes: []string{}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tenant := Tenant{Scopes: testCase.givenScopes} + assert.Equal(t, testCase.expectedScopes, tenant.additionalRequestedScopes()) + }) + } +} diff --git a/internal/cli/login.go b/internal/cli/login.go index 0300c243d..0ac82ad8c 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -112,7 +112,7 @@ auth0 login --scopes "read:client_grants,create:client_grants"`, shouldLoginAsUser := (cli.noInput && !inputs.isLoggingInAsAMachine()) || inputs.isLoggingInWithAdditionalScopes() || selectedLoginType == loginAsUser if shouldLoginAsUser { if _, err := RunLoginAsUser(ctx, cli, inputs.AdditionalScopes); err != nil { - return err + return fmt.Errorf("failed to start the authentication process: %w", err) } } else { if err := RunLoginAsMachine(ctx, inputs, cli, cmd); err != nil { @@ -147,7 +147,7 @@ auth0 login --scopes "read:client_grants,create:client_grants"`, func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string) (Tenant, error) { state, err := cli.authenticator.GetDeviceCode(ctx, additionalScopes) if err != nil { - return Tenant{}, fmt.Errorf("Failed to start the authentication process: %w.", err) + return Tenant{}, fmt.Errorf("failed to get the device code: %w", err) } message := fmt.Sprintf("\n%s\n%s%s\n\n", @@ -201,7 +201,7 @@ func RunLoginAsUser(ctx context.Context, cli *cli, additionalScopes []string) (T Domain: result.Domain, AccessToken: result.AccessToken, ExpiresAt: result.ExpiresAt, - Scopes: auth.RequiredScopes(), + Scopes: append(auth.RequiredScopes(), additionalScopes...), } err = cli.addTenant(tenant) From 03bea623c81f9136cec718c2a006a0d7e94a6c47 Mon Sep 17 00:00:00 2001 From: Will Vedder Date: Fri, 16 Dec 2022 12:25:34 -0500 Subject: [PATCH 12/13] Fixing linting error --- internal/cli/cli_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 3fb0adafe..ce86c6e0d 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -134,7 +134,7 @@ func TestTenant_AdditionalRequestedScopes(t *testing.T) { }, { name: "it returns an empty string slice if no additional requested scopes were given", - givenScopes: append(auth.RequiredScopes()), + givenScopes: auth.RequiredScopes(), expectedScopes: []string{}, }, } From cc625013b792c62f3d0472465798c966da7bc92c Mon Sep 17 00:00:00 2001 From: Sergiu Ghitea <28300158+sergiught@users.noreply.github.com> Date: Fri, 16 Dec 2022 18:39:01 +0100 Subject: [PATCH 13/13] Update internal/auth/auth.go --- internal/auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 093a8bcfe..bcbbcdd62 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -183,7 +183,7 @@ func (a *Authenticator) GetDeviceCode(ctx context.Context, additionalScopes []st strings.NewReader(data.Encode()), ) if err != nil { - return State{}, fmt.Errorf("failed to create the request: %w, ", err) + return State{}, fmt.Errorf("failed to create the request: %w", err) } request.Header.Set("Content-Type", "application/x-www-form-urlencoded")