Skip to content
5 changes: 4 additions & 1 deletion cli/azd/cmd/auth_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,13 @@ func (a *authStatusAction) Run(ctx context.Context) (*actions.ActionResult, erro
res.Status = contracts.AuthStatusUnauthenticated
} else {
res.Status = contracts.AuthStatusAuthenticated
_, err := a.verifyLoggedIn(ctx, scopes)
token, err := a.verifyLoggedIn(ctx, scopes)
if err != nil {
res.Status = contracts.AuthStatusUnauthenticated
log.Printf("error: verifying logged in status: %v", err)
} else if token != nil {
expiresOn := contracts.RFC3339Time(token.ExpiresOn)
res.ExpiresOn = &expiresOn
}

switch details.LoginType {
Expand Down
10 changes: 10 additions & 0 deletions cli/azd/cmd/middleware/login_guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package middleware

import (
"context"
"errors"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/internal/tracing/resource"
"github.com/azure/azure-dev/cli/azd/pkg/auth"
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
Expand Down Expand Up @@ -53,6 +55,14 @@ func (l *LoginGuardMiddleware) Run(ctx context.Context, next NextFn) (*actions.A

_, err = auth.EnsureLoggedInCredential(ctx, cred, l.authManager.Cloud())
if err != nil {
// Only wrap auth-specific errors with login guidance.
// Let cancellations, network errors, and transient failures propagate unchanged.
if errors.Is(err, auth.ErrNoCurrentUser) {
return nil, &internal.ErrorWithSuggestion{
Err: err,
Suggestion: "Run 'azd auth login' to sign in before running this command.",
}
}
return nil, err
}

Expand Down
3 changes: 3 additions & 0 deletions cli/azd/pkg/contracts/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ type StatusResult struct {

// The client ID of the service principal. Only set when Type is AccountTypeServicePrincipal.
ClientID string `json:"clientId,omitempty"`

// When authenticated, the time at which the current access token expires.
ExpiresOn *RFC3339Time `json:"expiresOn,omitempty"`
}
44 changes: 44 additions & 0 deletions cli/azd/pkg/contracts/auth_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,47 @@ func TestRFC3339TimeJson(t *testing.T) {
assert.Equal(t, `"2023-01-09T06:39:00.313323855Z"`, string(stdRes))
assert.Equal(t, `"2023-01-09T06:39:00Z"`, string(cusRes))
}

func TestStatusResultJsonWithExpiresOn(t *testing.T) {
tm, err := time.Parse(time.RFC3339, "2026-03-22T14:30:00Z")
require.NoError(t, err)

expiresOn := RFC3339Time(tm)
res := StatusResult{
Status: AuthStatusAuthenticated,
Type: AccountTypeUser,
Email: "user@example.com",
ExpiresOn: &expiresOn,
}

data, err := json.Marshal(res)
require.NoError(t, err)

var parsed StatusResult
err = json.Unmarshal(data, &parsed)
require.NoError(t, err)

assert.Equal(t, AuthStatusAuthenticated, parsed.Status)
assert.Equal(t, AccountTypeUser, parsed.Type)
assert.Equal(t, "user@example.com", parsed.Email)
require.NotNil(t, parsed.ExpiresOn)
assert.Equal(t, tm, time.Time(*parsed.ExpiresOn))
}

func TestStatusResultJsonWithoutExpiresOn(t *testing.T) {
res := StatusResult{
Status: AuthStatusUnauthenticated,
}

data, err := json.Marshal(res)
require.NoError(t, err)

assert.NotContains(t, string(data), "expiresOn")

var parsed StatusResult
err = json.Unmarshal(data, &parsed)
require.NoError(t, err)

assert.Equal(t, AuthStatusUnauthenticated, parsed.Status)
assert.Nil(t, parsed.ExpiresOn)
}
Loading