diff --git a/cli/azd/pkg/auth/errors.go b/cli/azd/pkg/auth/errors.go index e774e460198..5961d3b6ebe 100644 --- a/cli/azd/pkg/auth/errors.go +++ b/cli/azd/pkg/auth/errors.go @@ -11,6 +11,7 @@ import ( "slices" msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors" + "github.com/azure/azure-dev/cli/azd/internal" "github.com/azure/azure-dev/cli/azd/pkg/cloud" ) @@ -27,6 +28,10 @@ type ReLoginRequiredError struct { // The scenario in which the login is required scenario string + + errText string + + helpLink string } // newReLoginRequiredError returns an error if the response indicates that the user needs to reauthenticate. @@ -47,13 +52,21 @@ func newReLoginRequiredError( "interaction_required": err := ReLoginRequiredError{} err.init(response, scopes, cloud) - return &err, true + suggestion := fmt.Sprintf("Suggestion: %s, run `%s` to acquire a new token.", err.scenario, err.loginCmd) + if err.helpLink != "" { + suggestion += fmt.Sprintf(" See %s for more info.", err.helpLink) + } + return &internal.ErrorWithSuggestion{ + Err: &err, + Suggestion: suggestion, + }, true } return nil, false } func (e *ReLoginRequiredError) init(response *AadErrorResponse, scopes []string, cloud *cloud.Cloud) { + e.errText = response.ErrorDescription e.scenario = "reauthentication required" e.loginCmd = "azd auth login" if !matchesLoginScopes(scopes, cloud) { // if matching default login scopes, no scopes need to be specified @@ -62,13 +75,21 @@ func (e *ReLoginRequiredError) init(response *AadErrorResponse, scopes []string, } } + // The refresh token has expired or is invalid due to sign-in frequency checks by Conditional Access. if slices.Contains(response.ErrorCodes, 70043) { e.scenario = "login expired" } + + // In a Codespaces environment, `azd auth login` defaults to device code flow, which can cause issues + // getting tokens if the Entra tenant has Conditional Access Policies set. + if slices.Contains(response.ErrorCodes, 50005) { + e.loginCmd += " --use-device-code=false" + e.helpLink = "https://aka.ms/azd/troubleshoot/conditional-access-policy" + } } func (e *ReLoginRequiredError) Error() string { - return fmt.Sprintf("%s, run `%s` to log in", e.scenario, e.loginCmd) + return e.errText } // matchesLoginScopes checks if the elements contained in the slice match the scopes acquired during login. diff --git a/cli/azd/pkg/auth/errors_test.go b/cli/azd/pkg/auth/errors_test.go index d0f77e2ab81..5c1256923bd 100644 --- a/cli/azd/pkg/auth/errors_test.go +++ b/cli/azd/pkg/auth/errors_test.go @@ -100,3 +100,35 @@ func TestReLoginRequired(t *testing.T) { }) } } + +func TestReLoginRequiredError(t *testing.T) { + tests := []struct { + name string + resp *AadErrorResponse + want string + }{ + { + "invalid_grant", + &AadErrorResponse{ + Error: "invalid_grant", + ErrorDescription: "description 1", + }, + "description 1", + }, + { + "interaction_required", + &AadErrorResponse{ + Error: "interaction_required", + ErrorDescription: "description 2", + }, + "description 2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err, _ := newReLoginRequiredError(tt.resp, LoginScopes(cloud.AzurePublic()), cloud.AzurePublic()) + got := err.Error() + require.Equal(t, tt.want, got) + }) + } +}