From 604fdfa1aa11be40083febaa104f8993ff5d3699 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 28 May 2026 23:40:47 +0000 Subject: [PATCH 1/3] Fix azd auth status reporting unauthenticated in Cloud Shell LogInDetails was the only user-state auth method in auth.Manager missing a Cloud Shell fallback. Its siblings CredentialForCurrentUser and GetLoggedInServicePrincipalTenantID both fall back to the ambient Cloud Shell credential when readUserProperties returns ErrNoCurrentUser, but LogInDetails returned the error because in Cloud Shell the user never runs `azd auth login`. This single gap caused two symptoms in Cloud Shell: - `azd provision` failed because CurrentPrincipalType calls LogInDetails. - `azd ai agent init` blocked because it parses `azd auth status`, which reported unauthenticated. Add a Cloud Shell fallback to LogInDetails that derives the user identity from the ambient credential, reporting a User (email) login with a best-effort account from token claims. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/auth/manager.go | 20 ++++++++++ cli/azd/pkg/auth/manager_test.go | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index f91566c8c3c..45eaa063647 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1462,6 +1462,11 @@ func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { currentUser, err := readUserProperties(cfg) if err != nil { + // In Cloud Shell azd uses the ambient credential, so report that user + // rather than treating the session as unauthenticated. + if runcontext.IsRunningInCloudShell() { + return m.cloudShellLogInDetails(ctx) + } return nil, ErrNoCurrentUser } @@ -1488,6 +1493,21 @@ func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { return nil, ErrNoCurrentUser } +// cloudShellLogInDetails reports the Cloud Shell user, derived from the ambient +// credential. The session is always a valid user, so an empty account (no +// username claim) is not an error. +func (m *Manager) cloudShellLogInDetails(ctx context.Context) (*LogInDetails, error) { + claims, err := m.ClaimsForCurrentUser(ctx, nil) + if err != nil { + return nil, fmt.Errorf("fetching claims for Cloud Shell user: %w", err) + } + + return &LogInDetails{ + LoginType: EmailLoginType, + Account: strings.TrimSpace(claims.DisplayUsername()), + }, nil +} + type AuthSource string const ( diff --git a/cli/azd/pkg/auth/manager_test.go b/cli/azd/pkg/auth/manager_test.go index d29ca303d65..946b3187c93 100644 --- a/cli/azd/pkg/auth/manager_test.go +++ b/cli/azd/pkg/auth/manager_test.go @@ -413,6 +413,71 @@ func TestLogInDetails(t *testing.T) { require.Equal(t, "user@contoso.com", details.Account) }) + t.Run("cloud shell - returns user login type from token claims", func(t *testing.T) { + t.Setenv(runcontext.AzdInCloudShellEnvVar, "1") + + // Build an access token with a username claim and mock the Cloud Shell + // token endpoint to return it. + token := buildTestJWT(t, map[string]any{ + "unique_name": "user@contoso.com", + "oid": "oid-abc", + "tid": "tenant-xyz", + }) + + mockContext := mocks.NewMockContext(t.Context()) + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.URL.String() == "http://localhost:50342/oauth2/token" + }).Respond(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString( + fmt.Sprintf(`{"access_token":"%s","expires_on":"4070908800"}`, token))), + }) + + m := Manager{ + configManager: newMemoryConfigManager(), + userConfigManager: newMemoryUserConfigManager(), + httpClient: mockContext.HttpClient, + cloud: cloud.AzurePublic(), + } + + details, err := m.LogInDetails(t.Context()) + require.NoError(t, err) + require.Equal(t, EmailLoginType, details.LoginType) + require.Equal(t, "user@contoso.com", details.Account) + }) + + t.Run("cloud shell - authenticated even when token has no username claim", func(t *testing.T) { + t.Setenv(runcontext.AzdInCloudShellEnvVar, "1") + + // A Cloud Shell session is always a valid authenticated user, even if + // the token does not expose a username claim. + token := buildTestJWT(t, map[string]any{ + "oid": "oid-abc", + "tid": "tenant-xyz", + }) + + mockContext := mocks.NewMockContext(t.Context()) + mockContext.HttpClient.When(func(request *http.Request) bool { + return request.URL.String() == "http://localhost:50342/oauth2/token" + }).Respond(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString( + fmt.Sprintf(`{"access_token":"%s","expires_on":"4070908800"}`, token))), + }) + + m := Manager{ + configManager: newMemoryConfigManager(), + userConfigManager: newMemoryUserConfigManager(), + httpClient: mockContext.HttpClient, + cloud: cloud.AzurePublic(), + } + + details, err := m.LogInDetails(t.Context()) + require.NoError(t, err) + require.Equal(t, EmailLoginType, details.LoginType) + require.Empty(t, details.Account) + }) + t.Run("external auth - error when token has no usable account identifier", func(t *testing.T) { // Build a JWT token with no username claims token := buildTestJWT(t, map[string]any{ From 9f19e7104a728dfd84e2dbc50bc39e31fb622d8d Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 29 May 2026 22:55:14 +0000 Subject: [PATCH 2/3] Gate Cloud Shell LogInDetails fallback on ErrNoCurrentUser Only fall back to the ambient Cloud Shell credential when readUserProperties reports no current user. Other errors (e.g. corrupted stored user properties) now surface as a wrapped error instead of being masked by the fallback or remapped to ErrNoCurrentUser, so real config corruption isn't silently hidden. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/auth/manager.go | 13 +++++++++---- cli/azd/pkg/auth/manager_test.go | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index 45eaa063647..d75ac6b8dab 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1463,11 +1463,16 @@ func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { currentUser, err := readUserProperties(cfg) if err != nil { // In Cloud Shell azd uses the ambient credential, so report that user - // rather than treating the session as unauthenticated. - if runcontext.IsRunningInCloudShell() { - return m.cloudShellLogInDetails(ctx) + // rather than treating the session as unauthenticated. Only fall back + // when there is genuinely no logged-in user; other errors (e.g. corrupted + // stored user properties) should surface so they aren't silently hidden. + if errors.Is(err, ErrNoCurrentUser) { + if runcontext.IsRunningInCloudShell() { + return m.cloudShellLogInDetails(ctx) + } + return nil, ErrNoCurrentUser } - return nil, ErrNoCurrentUser + return nil, fmt.Errorf("reading current user properties: %w", err) } if currentUser.HomeAccountID != nil { diff --git a/cli/azd/pkg/auth/manager_test.go b/cli/azd/pkg/auth/manager_test.go index 946b3187c93..d47c6e00f06 100644 --- a/cli/azd/pkg/auth/manager_test.go +++ b/cli/azd/pkg/auth/manager_test.go @@ -478,6 +478,29 @@ func TestLogInDetails(t *testing.T) { require.Empty(t, details.Account) }) + t.Run("cloud shell - corrupted user properties surface error instead of fallback", func(t *testing.T) { + t.Setenv(runcontext.AzdInCloudShellEnvVar, "1") + + // A stored currentUser value that cannot be unmarshalled into userProperties + // represents real config corruption. It must surface as an error rather than + // being silently masked by the Cloud Shell fallback. + userCfg := config.NewEmptyConfig() + require.NoError(t, userCfg.Set(currentUserKey, "not-an-object")) + + userCfgMgr := newMemoryUserConfigManager() + require.NoError(t, userCfgMgr.Save(userCfg)) + + m := Manager{ + configManager: newMemoryConfigManager(), + userConfigManager: userCfgMgr, + cloud: cloud.AzurePublic(), + } + + _, err := m.LogInDetails(t.Context()) + require.Error(t, err) + require.NotErrorIs(t, err, ErrNoCurrentUser) + }) + t.Run("external auth - error when token has no usable account identifier", func(t *testing.T) { // Build a JWT token with no username claims token := buildTestJWT(t, map[string]any{ From d4c6fa22e673d2f0a26b56cedb4e12f3cc96b489 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Tue, 2 Jun 2026 19:58:36 +0000 Subject: [PATCH 3/3] Address PR feedback: update LogInDetails Godoc and test wrap assertion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cli/azd/pkg/auth/manager.go | 3 +++ cli/azd/pkg/auth/manager_test.go | 1 + 2 files changed, 4 insertions(+) diff --git a/cli/azd/pkg/auth/manager.go b/cli/azd/pkg/auth/manager.go index d75ac6b8dab..98cc05afb36 100644 --- a/cli/azd/pkg/auth/manager.go +++ b/cli/azd/pkg/auth/manager.go @@ -1402,6 +1402,9 @@ type LogInDetails struct { // return the account name from the az CLI. When external authentication is // configured, it will acquire a token from the external auth endpoint (an // outbound HTTP call) and derive the account identifier from the token claims. +// When running in Azure Cloud Shell and no azd-managed user is logged in, +// it derives the account from the ambient Cloud Shell credential and reports +// an authenticated user. func (m *Manager) LogInDetails(ctx context.Context) (*LogInDetails, error) { if m.UseExternalAuth() { claims, err := m.ClaimsForCurrentUser(ctx, nil) diff --git a/cli/azd/pkg/auth/manager_test.go b/cli/azd/pkg/auth/manager_test.go index d47c6e00f06..fdf0d7e5611 100644 --- a/cli/azd/pkg/auth/manager_test.go +++ b/cli/azd/pkg/auth/manager_test.go @@ -499,6 +499,7 @@ func TestLogInDetails(t *testing.T) { _, err := m.LogInDetails(t.Context()) require.Error(t, err) require.NotErrorIs(t, err, ErrNoCurrentUser) + require.ErrorContains(t, err, "reading current user properties") }) t.Run("external auth - error when token has no usable account identifier", func(t *testing.T) {