Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions cli/azd/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,13 @@ func authActions(root *actions.ActionDescriptor) *actions.ActionDescriptor {
ActionResolver: newLogoutAction,
})

group.Add("status", &actions.ActionDescriptorOptions{
Command: newAuthStatusCmd(),
FlagsResolver: newAuthStatusFlags,
ActionResolver: newAuthStatusAction,
OutputFormats: []output.Format{output.JsonFormat, output.NoneFormat},
DefaultFormat: output.NoneFormat,
})

return group
}
9 changes: 9 additions & 0 deletions cli/azd/cmd/auth_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ func (la *loginAction) Run(ctx context.Context) (*actions.ActionResult, error) {
LoggedInAs: details.Account,
LoginType: ux.LoginType(details.LoginType),
})

// Display token expiration information
if res.ExpiresOn != nil {
expiryMsg := fmt.Sprintf(
"\nCredential expires on: %s",
res.ExpiresOn.Format("2006-01-02 15:04:05 MST"))
la.console.Message(ctx, expiryMsg)
}

return nil, nil
}
}
Expand Down
153 changes: 153 additions & 0 deletions cli/azd/cmd/auth_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"errors"
"fmt"
"io"
"log"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/auth"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/azure/azure-dev/cli/azd/pkg/output/ux"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

type authStatusFlags struct {
global *internal.GlobalCommandOptions
}

func newAuthStatusFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *authStatusFlags {
flags := &authStatusFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}

func (f *authStatusFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.global = global
}

func newAuthStatusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Check the authentication status.",
Long: "Check the authentication status. Returns information about the logged-in user and when credentials expire.",
}
}

type authStatusAction struct {
formatter output.Formatter
writer io.Writer
console input.Console
authManager *auth.Manager
flags *authStatusFlags
}

func newAuthStatusAction(
formatter output.Formatter,
writer io.Writer,
authManager *auth.Manager,
flags *authStatusFlags,
console input.Console,
) actions.Action {
return &authStatusAction{
formatter: formatter,
writer: writer,
console: console,
authManager: authManager,
flags: flags,
}
}

func (a *authStatusAction) Run(ctx context.Context) (*actions.ActionResult, error) {
scopes := a.authManager.LoginScopes()

// In check status mode, we always print the final status to stdout.
// We print any non-setup related errors to stderr.
// We always return a zero exit code.
Comment on lines +75 to +77
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "In check status mode, we always print the final status to stdout" appears to be copied from auth_login.go where it makes sense in the context of the --check-status flag. However, for the dedicated auth_status command, this comment is slightly misleading since the command is always in "status mode" - there's no mode switching.

Consider updating the comment to better reflect that this is the primary behavior of the status command, for example: "The status command always prints the final status to stdout and returns a zero exit code. Any non-setup related errors are printed to stderr."

Copilot uses AI. Check for mistakes.
token, err := a.verifyLoggedIn(ctx, scopes)
var loginExpiryError *auth.ReLoginRequiredError
if err != nil &&
!errors.Is(err, auth.ErrNoCurrentUser) &&
!errors.As(err, &loginExpiryError) {
fmt.Fprintln(a.console.Handles().Stderr, err.Error())
}

res := contracts.LoginResult{}
if err != nil {
res.Status = contracts.LoginStatusUnauthenticated
} else {
res.Status = contracts.LoginStatusSuccess
res.ExpiresOn = &token.ExpiresOn
}

if a.formatter.Kind() != output.NoneFormat {
return nil, a.formatter.Format(res, a.writer, nil)
} else {
var msg string
switch res.Status {
case contracts.LoginStatusSuccess:
msg = "Logged in to Azure"
case contracts.LoginStatusUnauthenticated:
msg = "Not logged in, run `azd auth login` to login to Azure"
default:
panic("Unhandled login status")
}

// get user account information
details, err := a.authManager.LogInDetails(ctx)

// error getting user account or not logged in
if err != nil {
log.Printf("error: getting signed in account: %v", err)
fmt.Fprintln(a.console.Handles().Stdout, msg)
return nil, nil
}

// only print the message if the user is logged in
a.console.MessageUxItem(ctx, &ux.LoggedIn{
LoggedInAs: details.Account,
LoginType: ux.LoginType(details.LoginType),
})

// Display token expiration information
if res.ExpiresOn != nil {
expiryMsg := fmt.Sprintf(
"\nCredential expires on: %s",
res.ExpiresOn.Format("2006-01-02 15:04:05 MST"))
a.console.Message(ctx, expiryMsg)
}

return nil, nil
}
Comment on lines +72 to +132
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire status-checking logic block (lines 72-132) is duplicated from auth_login.go lines 305-363. The code is nearly identical except for the verifyLoggedIn method signature. This violates the DRY principle mentioned in the PR instructions.

Consider extracting the common status-checking logic into a shared helper function that both auth_login.go (--check-status flag) and auth_status.go can use. This would:

  1. Reduce maintenance burden when the logic needs to change
  2. Ensure consistency between both commands
  3. Make the codebase easier to maintain

The helper function could accept the necessary parameters (formatter, writer, console, authManager, token, err) and handle the status display logic.

Copilot uses AI. Check for mistakes.
}

// Verifies that the user has credentials stored,
// and that the credentials stored is accepted by the identity server (can be exchanged for access token).
func (a *authStatusAction) verifyLoggedIn(ctx context.Context, scopes []string) (*azcore.AccessToken, error) {
cred, err := a.authManager.CredentialForCurrentUser(ctx, nil)
if err != nil {
return nil, err
}

// Ensure credential is valid, and can be exchanged for an access token
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: scopes,
})

if err != nil {
return nil, err
}

return &token, nil
}
Comment on lines +72 to +153
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new auth_status command lacks unit tests. Similar commands in the same directory have comprehensive test coverage. For example, auth_token_test.go has 367 lines of tests covering various scenarios including success cases, error cases, and different input combinations.

At minimum, tests should cover:

  1. Authenticated user scenario (status success with expiration)
  2. Unauthenticated user scenario (status unauthenticated)
  3. JSON output format validation
  4. Text output format validation
  5. Error scenarios (e.g., ReLoginRequiredError, ErrNoCurrentUser)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wbreza - any reason not to implement this suggestion to add tests?

8 changes: 8 additions & 0 deletions cli/azd/cmd/testdata/TestFigSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ const completionSpec: Fig.Spec = {
name: ['logout'],
description: 'Log out of Azure.',
},
{
name: ['status'],
description: 'Check the authentication status.',
},
],
},
{
Expand Down Expand Up @@ -1540,6 +1544,10 @@ const completionSpec: Fig.Spec = {
name: ['logout'],
description: 'Log out of Azure.',
},
{
name: ['status'],
description: 'Check the authentication status.',
},
],
},
{
Expand Down
16 changes: 16 additions & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

Check the authentication status.

Usage
azd auth status [flags]

Global Flags
-C, --cwd string : Sets the current working directory.
--debug : Enables debugging and diagnostics logging.
--docs : Opens the documentation for azd auth status in your web browser.
-h, --help : Gets help for status.
--no-prompt : Accepts the default value instead of prompting, or it fails if there is no default.

Find a bug? Want to let us know how we're doing? Fill out this brief survey: https://aka.ms/azure-dev/hats.


1 change: 1 addition & 0 deletions cli/azd/cmd/testdata/TestUsage-azd-auth.snap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Usage
Available Commands
login : Log in to Azure.
logout : Log out of Azure.
status : Check the authentication status.

Global Flags
-C, --cwd string : Sets the current working directory.
Expand Down
Loading