Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 20 additions & 0 deletions cli/azd/pkg/auth/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
JeffreyCA marked this conversation as resolved.
// rather than treating the session as unauthenticated.
if runcontext.IsRunningInCloudShell() {
return m.cloudShellLogInDetails(ctx)
}
return nil, ErrNoCurrentUser
Comment thread
JeffreyCA marked this conversation as resolved.
Outdated
}

Expand All @@ -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 (
Expand Down
65 changes: 65 additions & 0 deletions cli/azd/pkg/auth/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading