Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3d5b12b
Add atlas auth connect command shell with UserDelegation telemetry
jricher May 8, 2026
fe83381
Wire discovery caching into the connect command
jricher May 9, 2026
f9780c4
Wire authorization code flow into the connect command
jricher May 9, 2026
1d80dd0
Add issuer-based cache validation and config overrides to connect com…
jricher May 9, 2026
eaf9829
Store token expiry and add UserDelegation to organizations create
jricher May 9, 2026
b28f4ba
Add UserDelegation support to whoami command
jricher May 11, 2026
79107d2
Add --discover flag to force re-fetch of authorization server metadata
jricher May 11, 2026
e128b8a
Add UserDelegation support to logout command
jricher May 11, 2026
75900dd
Add UserDelegation to setup PreRun authenticated check
jricher May 11, 2026
09cb458
Add --noBrowser manual paste flow to connect command
jricher May 12, 2026
72f7b70
Realign ConnectOpts struct fields with gofmt
jricher May 13, 2026
4042798
Regenerate logout mock after ConfigDeleter grew UserDelegation methods
jricher May 13, 2026
08ddbdd
Trim restated comments in connect.go
jricher May 13, 2026
b7f319b
Pass os.Stdin to ParseCodeFromRedirectURL
jricher May 13, 2026
f37be5d
Add ConnectConfig mock generation
jricher May 13, 2026
db3e129
Make UserDelegation revoke injectable in logout
jricher May 14, 2026
a69e4c4
Pass token state into whoOpts instead of reading globals
jricher May 14, 2026
dc67d41
Add unit tests for connect, logout, and whoami UserDelegation paths
jricher May 14, 2026
0c979a6
Consolidate whoami dispatch into Run
jricher May 19, 2026
fa31f00
Add new menu item for connect command under login, uses existing mach…
jricher Jun 25, 2026
c2ed9f9
Fix gofmt alignment in login and prompt auth constants
jricher Jun 25, 2026
c596330
Rename connect command to UserDelegationFlow
jricher Jun 25, 2026
4dd760d
Gate device-flow refresh to UserAccount profiles
jricher Jun 25, 2026
39ea9da
Run the authorization code flow from the login menu
jricher Jun 25, 2026
5da2437
Wire logout revoke per auth type
jricher Jun 30, 2026
a428b08
Drop empty identifier from already-authenticated error
jricher Jun 30, 2026
3360f6b
Remove the standalone auth connect command
jricher Jun 30, 2026
be609dd
Prompt for the helper page code in the no-browser flow
jricher Aug 6, 2026
ca5fcd7
Reference the no-browser redirect URI constant
jricher Aug 7, 2026
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
4 changes: 4 additions & 0 deletions docs/command/atlas-auth-login.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Options
- Type
- Required
- Description
* - --discover
-
- false
- Force re-discovery of authorization server metadata.
* - --gov
-
- false
Expand Down
29 changes: 27 additions & 2 deletions internal/cli/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ type TrackAsker interface {
TrackAskOne(survey.Prompt, any, ...survey.AskOpt) error
}

type userDelegationRunner interface {
Run(ctx context.Context) error
}

const (
userAccountAuth = "UserAccount"
atlasName = "atlas"
Expand All @@ -75,9 +79,10 @@ const (
var (
ErrProjectIDNotFound = errors.New("project is inaccessible. You either don't have access to this project or the project doesn't exist")
ErrOrgIDNotFound = errors.New("organization is inaccessible. You don't have access to this organization or the organization doesn't exist")
authTypeOptions = []string{userAccountAuth, prompt.ServiceAccountAuth, prompt.APIKeysAuth}
authTypeOptions = []string{userAccountAuth, prompt.UserDelegationAuth, prompt.ServiceAccountAuth, prompt.APIKeysAuth}
authTypeDescription = map[string]string{
userAccountAuth: "(best for getting started)",
userAccountAuth: "(legacy account connection)",

@saisundar saisundar Aug 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I realize this is what we want to call out when things have stabilized, and verified as working cleanly - does this wording change (legacy vs “best for user accounts”) need to ship as part of this release?

@jricher jricher Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This label needs feedback from the CLI team (as does the rest of the user-facing components, like the callback page and paste-helper page) before release.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This major change would ideally be accompanied by a

  • rollback strategy and a rollout strategy. Could you link one ( can't find it in the linked tickets or TD/Scope) ?

There is the technical capability, and the product phrasing.
Phrasing this yet-to-be-validated-by-prod-traffic path as

" prompt.UserDelegationAuth: "(best for user accounts)",

during its v0 commit is heavy-handed.

prompt.UserDelegationAuth: "(best for user accounts)",
prompt.ServiceAccountAuth: "(best for automation)",
prompt.APIKeysAuth: "(for existing automations)",
}
Expand All @@ -94,11 +99,14 @@ type LoginOpts struct {
PrivateAPIKey string
IsGov bool
NoBrowser bool
Discover bool
authType string
force bool
SkipConfig bool
config LoginConfig
Asker TrackAsker

userDelegationFlow userDelegationRunner
}

func (opts *LoginOpts) promptAuthType() error {
Expand Down Expand Up @@ -165,13 +173,27 @@ func (opts *LoginOpts) setUpCredentials(ctx context.Context) error {
switch opts.authType {
case userAccountAuth:
return opts.setUserAccountCredentials(ctx)
case prompt.UserDelegationAuth:
return opts.runUserDelegationFlow(ctx)
case prompt.ServiceAccountAuth, prompt.APIKeysAuth:
return opts.setProgrammaticCredentials()
default:
return errors.New("no authentication type selected")
}
}

func (opts *LoginOpts) runUserDelegationFlow(ctx context.Context) error {
if opts.userDelegationFlow == nil {
opts.userDelegationFlow = &UserDelegationFlow{
config: config.Default(),
OutWriter: opts.OutWriter,
NoBrowser: opts.NoBrowser,
Discover: opts.Discover,
}
}
return opts.userDelegationFlow.Run(ctx)
}

func (opts *LoginOpts) setUpAccess() {
// Set service
switch {
Expand Down Expand Up @@ -246,6 +268,8 @@ func (opts *LoginOpts) LoginRun(ctx context.Context) error {
switch opts.authType {
case userAccountAuth:
opts.config.SetAuthType(config.UserAccount)
case prompt.UserDelegationAuth:
opts.config.SetAuthType(config.UserDelegation)
case prompt.ServiceAccountAuth:
opts.config.SetAuthType(config.ServiceAccount)
case prompt.APIKeysAuth:
Expand Down Expand Up @@ -468,6 +492,7 @@ func LoginBuilder() *cobra.Command {

cmd.Flags().BoolVar(&opts.IsGov, "gov", false, "Log in to Atlas for Government.")
cmd.Flags().BoolVar(&opts.NoBrowser, "noBrowser", false, "Don't automatically open a browser session.")
cmd.Flags().BoolVar(&opts.Discover, "discover", false, "Force re-discovery of authorization server metadata.")
cmd.Flags().BoolVar(&opts.SkipConfig, "skipConfig", false, "Skip profile configuration.")
_ = cmd.Flags().MarkDeprecated("skipConfig", "if you configured a profile, the command skips the config step by default.")
cmd.Flags().BoolVar(&opts.force, flag.Force, false, usage.Force)
Expand Down
41 changes: 41 additions & 0 deletions internal/cli/auth/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package auth

import (
"bytes"
"context"
"errors"
"fmt"
"testing"
Expand Down Expand Up @@ -137,6 +138,46 @@ func Test_loginOpts_LoginRun_UserAccount(t *testing.T) {
require.NoError(t, err)
}

type fakeUserDelegationFlow struct {
called bool
}

func (f *fakeUserDelegationFlow) Run(context.Context) error {
f.called = true
return nil
}

func Test_loginOpts_LoginRun_UserDelegation(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := NewMockLoginConfig(ctrl)
mockAsker := NewMockTrackAsker(ctrl)

flow := &fakeUserDelegationFlow{}
opts := &LoginOpts{
config: mockConfig,
Asker: mockAsker,
userDelegationFlow: flow,
}
opts.OutWriter = new(bytes.Buffer)

mockAsker.EXPECT().
TrackAskOne(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ survey.Prompt, answer any, _ ...survey.AskOpt) error {
if s, ok := answer.(*string); ok {
*s = prompt.UserDelegationAuth
}
return nil
})

mockConfig.EXPECT().SetAuthType(config.UserDelegation).Times(1)

opts.SkipConfig = true

err := opts.LoginRun(t.Context())
require.NoError(t, err)
assert.True(t, flow.called)
}

func TestLoginRun_APIKeys_Success(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := NewMockLoginConfig(ctrl)
Expand Down
56 changes: 49 additions & 7 deletions internal/cli/auth/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package auth

import (
"context"
"errors"
"io"
"net/http"
"strings"
Expand Down Expand Up @@ -52,6 +53,9 @@ type ConfigDeleter interface {
ClientSecret() string
AccessTokenSubject() (string, error)
RefreshToken() string
Service() string
AuthServerURL() string
AuthServerMetadata() map[string]any
Save() error
}

Expand All @@ -67,16 +71,14 @@ type logoutOpts struct {
flow Revoker
keepConfig bool
revokeServiceAccountToken func() error
revokeAuthServerToken func(context.Context) error
}

func (opts *logoutOpts) initFlow(ctx context.Context) error {
func (opts *logoutOpts) initFlow() error {
var err error
client := http.DefaultClient
client.Transport = transport.Default()
opts.flow, err = transport.FlowWithConfig(config.Default(), client, version.Version)
opts.revokeServiceAccountToken = func() error {
return revokeServiceAccountToken(ctx, opts.config.ClientID(), opts.config.ClientSecret())
}
return err
}

Expand All @@ -95,6 +97,31 @@ func revokeServiceAccountToken(ctx context.Context, clientID, clientSecret strin
return cfg.RevokeToken(ctx, token)
}

func revokeAuthServerToken(ctx context.Context, cfg ConfigDeleter) error {
metadata := cfg.AuthServerMetadata()
if metadata == nil {
return errors.New("no auth server metadata available")
}
m, ok := metadata["metadata"].(map[string]any)
if !ok {
return errors.New("invalid auth server metadata")
}
revocationEndpoint, ok := m["revocation_endpoint"].(string)
if !ok || revocationEndpoint == "" {
return errors.New("revocation_endpoint not found in auth server metadata")
}

client := http.DefaultClient
client.Transport = transport.Default()

authCfg, err := transport.FlowForAuthIssuer(cfg, client, version.Version)
if err != nil {
return err
}

return authCfg.RevokeAuthServerToken(ctx, revocationEndpoint, cfg.RefreshToken(), "refresh_token")
}

func (opts *logoutOpts) Run(ctx context.Context) error {
if !opts.Confirm {
return nil
Expand All @@ -106,6 +133,10 @@ func (opts *logoutOpts) Run(ctx context.Context) error {
if err != nil {
_, _ = log.Warningf("Warning: unable to revoke user account token: %v, proceeding with logout\n", err)
}
case config.UserDelegation:
if err := opts.revokeAuthServerToken(ctx); err != nil {
_, _ = log.Warningf("Warning: unable to revoke token: %v, proceeding with logout\n", err)
}
case config.ServiceAccount:
if err := opts.revokeServiceAccountToken(); err != nil {
_, _ = log.Warningf("Warning: unable to revoke service account token: %v, proceeding with logout\n", err)
Expand Down Expand Up @@ -155,9 +186,18 @@ func LogoutBuilder() *cobra.Command {
opts.config = config.Default()
}

// Only initialize OAuth flow if we have OAuth-based auth
if opts.config.AuthType() == config.UserAccount || opts.config.AuthType() == config.ServiceAccount {
return opts.initFlow(cmd.Context())
// Wire up the revoke path for the profile's auth type.
switch opts.config.AuthType() {
case config.UserAccount:
return opts.initFlow()
case config.ServiceAccount:
opts.revokeServiceAccountToken = func() error {
return revokeServiceAccountToken(cmd.Context(), opts.config.ClientID(), opts.config.ClientSecret())
}
case config.UserDelegation:
opts.revokeAuthServerToken = func(ctx context.Context) error {
return revokeAuthServerToken(ctx, opts.config)
}
}

return nil
Expand All @@ -184,6 +224,8 @@ func LogoutBuilder() *cobra.Command {
}

message = logoutMessage + " with user account " + subject + "?"
case config.UserDelegation:
message = logoutMessage + "?"
case config.NoAuth, "":
message = logoutMessage + "?"
}
Expand Down
114 changes: 114 additions & 0 deletions internal/cli/auth/logout_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading