From ab2990a9a8580fd3d705f1a5a3b7230d57c00c8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:29:56 +0000 Subject: [PATCH 1/5] Initial plan From abe856801764bc1c4ccad5da82a2ef26f092774d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:40:10 +0000 Subject: [PATCH 2/5] Add azd auth status command Co-authored-by: kristenwomack <5034778+kristenwomack@users.noreply.github.com> --- cli/azd/cmd/auth.go | 8 + cli/azd/cmd/auth_status.go | 163 ++++++++++++++++++ cli/azd/cmd/testdata/TestFigSpec.ts | 19 ++ .../testdata/TestUsage-azd-auth-status.snap | 19 ++ cli/azd/cmd/testdata/TestUsage-azd-auth.snap | 1 + 5 files changed, 210 insertions(+) create mode 100644 cli/azd/cmd/auth_status.go create mode 100644 cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap diff --git a/cli/azd/cmd/auth.go b/cli/azd/cmd/auth.go index 7f68cb507a1..20761b89edd 100644 --- a/cli/azd/cmd/auth.go +++ b/cli/azd/cmd/auth.go @@ -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 } diff --git a/cli/azd/cmd/auth_status.go b/cli/azd/cmd/auth_status.go new file mode 100644 index 00000000000..e6b177d8643 --- /dev/null +++ b/cli/azd/cmd/auth_status.go @@ -0,0 +1,163 @@ +// 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 { + tenantID string + scopes []string + 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) { + local.StringVar( + &f.tenantID, + "tenant-id", + "", + "The tenant id or domain name to authenticate with.") + local.StringArrayVar( + &f.scopes, + "scope", + nil, + "The scope to acquire during login") + _ = local.MarkHidden("scope") + 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) { + if len(a.flags.scopes) == 0 { + a.flags.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. + token, err := a.verifyLoggedIn(ctx) + 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), + }) + return nil, nil + } +} + +// 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) (*azcore.AccessToken, error) { + credOptions := auth.CredentialForCurrentUserOptions{ + TenantID: a.flags.tenantID, + } + + cred, err := a.authManager.CredentialForCurrentUser(ctx, &credOptions) + 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: a.flags.scopes, + }) + + if err != nil { + return nil, err + } + + return &token, nil +} diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index 11f41f1eb82..c01a139c1bb 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -277,6 +277,21 @@ const completionSpec: Fig.Spec = { name: ['logout'], description: 'Log out of Azure.', }, + { + name: ['status'], + description: 'Check the authentication status.', + options: [ + { + name: ['--tenant-id'], + description: 'The tenant id or domain name to authenticate with.', + args: [ + { + name: 'tenant-id', + }, + ], + }, + ], + }, ], }, { @@ -1540,6 +1555,10 @@ const completionSpec: Fig.Spec = { name: ['logout'], description: 'Log out of Azure.', }, + { + name: ['status'], + description: 'Check the authentication status.', + }, ], }, { diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap new file mode 100644 index 00000000000..8ee02cba968 --- /dev/null +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap @@ -0,0 +1,19 @@ + +Check the authentication status. + +Usage + azd auth status [flags] + +Flags + --tenant-id string : The tenant id or domain name to authenticate with. + +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. + + diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth.snap index 21a262409d3..b97ffc75d8a 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth.snap @@ -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. From 80ffa14ff191d1ba7d913a20996ca72388417a75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:43:44 +0000 Subject: [PATCH 3/5] Fix flag description for auth status scope parameter Co-authored-by: kristenwomack <5034778+kristenwomack@users.noreply.github.com> --- cli/azd/cmd/auth_status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/azd/cmd/auth_status.go b/cli/azd/cmd/auth_status.go index e6b177d8643..9158b21edb3 100644 --- a/cli/azd/cmd/auth_status.go +++ b/cli/azd/cmd/auth_status.go @@ -45,7 +45,7 @@ func (f *authStatusFlags) Bind(local *pflag.FlagSet, global *internal.GlobalComm &f.scopes, "scope", nil, - "The scope to acquire during login") + "The scope to use when verifying authentication status") _ = local.MarkHidden("scope") f.global = global } From bb9541f6762a9bbe340e94e9b4a0522fba498505 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:58:45 +0000 Subject: [PATCH 4/5] Remove tenant-id and scope flags from auth status, add expiration to text output Addresses review feedback: - Removed tenant-id and scope flags as they don't make sense for status checking - The command now checks the current user's credentials without allowing tenant/scope overrides - Added credential expiration time to text output (already present in JSON) - Simplified the implementation to always use default login scopes Co-authored-by: wbreza <6540159+wbreza@users.noreply.github.com> --- cli/azd/cmd/auth_status.go | 40 +++++++------------ cli/azd/cmd/testdata/TestFigSpec.ts | 11 ----- .../testdata/TestUsage-azd-auth-status.snap | 3 -- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/cli/azd/cmd/auth_status.go b/cli/azd/cmd/auth_status.go index 9158b21edb3..66289c1981b 100644 --- a/cli/azd/cmd/auth_status.go +++ b/cli/azd/cmd/auth_status.go @@ -24,9 +24,7 @@ import ( ) type authStatusFlags struct { - tenantID string - scopes []string - global *internal.GlobalCommandOptions + global *internal.GlobalCommandOptions } func newAuthStatusFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *authStatusFlags { @@ -36,17 +34,6 @@ func newAuthStatusFlags(cmd *cobra.Command, global *internal.GlobalCommandOption } func (f *authStatusFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) { - local.StringVar( - &f.tenantID, - "tenant-id", - "", - "The tenant id or domain name to authenticate with.") - local.StringArrayVar( - &f.scopes, - "scope", - nil, - "The scope to use when verifying authentication status") - _ = local.MarkHidden("scope") f.global = global } @@ -83,14 +70,12 @@ func newAuthStatusAction( } func (a *authStatusAction) Run(ctx context.Context) (*actions.ActionResult, error) { - if len(a.flags.scopes) == 0 { - a.flags.scopes = a.authManager.LoginScopes() - } + 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. - token, err := a.verifyLoggedIn(ctx) + token, err := a.verifyLoggedIn(ctx, scopes) var loginExpiryError *auth.ReLoginRequiredError if err != nil && !errors.Is(err, auth.ErrNoCurrentUser) && @@ -134,25 +119,30 @@ func (a *authStatusAction) Run(ctx context.Context) (*actions.ActionResult, erro 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 } } // 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) (*azcore.AccessToken, error) { - credOptions := auth.CredentialForCurrentUserOptions{ - TenantID: a.flags.tenantID, - } - - cred, err := a.authManager.CredentialForCurrentUser(ctx, &credOptions) +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: a.flags.scopes, + Scopes: scopes, }) if err != nil { diff --git a/cli/azd/cmd/testdata/TestFigSpec.ts b/cli/azd/cmd/testdata/TestFigSpec.ts index c01a139c1bb..99a4cfd712f 100644 --- a/cli/azd/cmd/testdata/TestFigSpec.ts +++ b/cli/azd/cmd/testdata/TestFigSpec.ts @@ -280,17 +280,6 @@ const completionSpec: Fig.Spec = { { name: ['status'], description: 'Check the authentication status.', - options: [ - { - name: ['--tenant-id'], - description: 'The tenant id or domain name to authenticate with.', - args: [ - { - name: 'tenant-id', - }, - ], - }, - ], }, ], }, diff --git a/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap b/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap index 8ee02cba968..934987fe0a4 100644 --- a/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap +++ b/cli/azd/cmd/testdata/TestUsage-azd-auth-status.snap @@ -4,9 +4,6 @@ Check the authentication status. Usage azd auth status [flags] -Flags - --tenant-id string : The tenant id or domain name to authenticate with. - Global Flags -C, --cwd string : Sets the current working directory. --debug : Enables debugging and diagnostics logging. From bec39dcab342e68d401df9069ae17334d369e22a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:57:22 +0000 Subject: [PATCH 5/5] Add credential expiration to azd auth login --check-status output For consistency with azd auth status, the --check-status flag now also displays the credential expiration time in the text output format. Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> --- cli/azd/cmd/auth_login.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cli/azd/cmd/auth_login.go b/cli/azd/cmd/auth_login.go index 69d5b2e4648..10f58af5bfa 100644 --- a/cli/azd/cmd/auth_login.go +++ b/cli/azd/cmd/auth_login.go @@ -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 } }