Skip to content

Commit 86ccab5

Browse files
committed
WIP
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
1 parent bab48eb commit 86ccab5

File tree

413 files changed

+57758
-4270
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

413 files changed

+57758
-4270
lines changed

cli/command/cli.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"github.com/docker/cli/cli/context/store"
2222
"github.com/docker/cli/cli/debug"
2323
cliflags "github.com/docker/cli/cli/flags"
24+
"github.com/docker/cli/cli/internal/oauth/manager"
2425
manifeststore "github.com/docker/cli/cli/manifest/store"
26+
"github.com/docker/cli/cli/oauth"
2527
registryclient "github.com/docker/cli/cli/registry/client"
2628
"github.com/docker/cli/cli/streams"
2729
"github.com/docker/cli/cli/trust"
@@ -32,6 +34,7 @@ import (
3234
"github.com/docker/docker/api/types/registry"
3335
"github.com/docker/docker/api/types/swarm"
3436
"github.com/docker/docker/client"
37+
dregistry "github.com/docker/docker/registry"
3538
"github.com/docker/go-connections/tlsconfig"
3639
"github.com/pkg/errors"
3740
"github.com/spf13/cobra"
@@ -66,6 +69,7 @@ type Cli interface {
6669
CurrentContext() string
6770
DockerEndpoint() docker.Endpoint
6871
TelemetryClient
72+
OAuthManager() oauth.Manager
6973
}
7074

7175
// DockerCli is an instance the docker command line client.
@@ -94,6 +98,8 @@ type DockerCli struct {
9498
baseCtx context.Context
9599

96100
enableGlobalMeter, enableGlobalTracer bool
101+
102+
oauthManager *manager.OAuthManager
97103
}
98104

99105
// DefaultVersion returns api.defaultVersion.
@@ -254,6 +260,10 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
254260
}
255261
}
256262

263+
func (cli *DockerCli) OAuthManager() oauth.Manager {
264+
return cli.oauthManager
265+
}
266+
257267
// Initialize the dockerCli runs initialization that must happen after command
258268
// line flags are parsed.
259269
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
@@ -293,6 +303,12 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
293303
cli.createGlobalTracerProvider(cli.baseCtx)
294304
}
295305

306+
oauthManager, err := manager.NewManager(cli.ConfigFile().GetCredentialsStore(dregistry.IndexServer))
307+
if err != nil {
308+
return err
309+
}
310+
cli.oauthManager = oauthManager
311+
296312
return nil
297313
}
298314

cli/command/registry.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,14 @@ import (
1111
"github.com/docker/cli/cli/config/configfile"
1212
"github.com/docker/cli/cli/config/credentials"
1313
configtypes "github.com/docker/cli/cli/config/types"
14-
"github.com/docker/cli/cli/hints"
14+
"github.com/docker/cli/cli/internal/oauth/util"
1515
"github.com/docker/cli/cli/streams"
1616
"github.com/docker/docker/api/types"
1717
registrytypes "github.com/docker/docker/api/types/registry"
1818
"github.com/docker/docker/registry"
1919
"github.com/pkg/errors"
2020
)
2121

22-
const patSuggest = "You can log in with your password or a Personal Access " +
23-
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
24-
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
25-
2622
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
2723
// for the given command.
2824
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
@@ -87,6 +83,8 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
8783
}
8884

8985
// ConfigureAuth handles prompting of user's username and password if needed
86+
//
87+
//nolint:gocyclo
9088
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
9189
// On Windows, force the use of the regular OS stdin stream.
9290
//
@@ -107,7 +105,7 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
107105
// Linux will hit this if you attempt `cat | docker login`, and Windows
108106
// will hit this if you attempt docker login from mintty where stdin
109107
// is a pipe, not a character based console.
110-
if flPassword == "" && !cli.In().IsTerminal() {
108+
if flPassword == "" && !isDefaultRegistry && !cli.In().IsTerminal() {
111109
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
112110
}
113111

@@ -117,10 +115,19 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
117115
if isDefaultRegistry {
118116
// if this is a default registry (docker hub), then display the following message.
119117
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
120-
if hints.Enabled() {
121-
fmt.Fprintln(cli.Out(), patSuggest)
122-
fmt.Fprintln(cli.Out())
118+
119+
res, err := cli.OAuthManager().LoginDevice(ctx, cli.Err())
120+
if err != nil {
121+
return err
123122
}
123+
claims, err := util.GetClaims(res.AccessToken)
124+
if err != nil {
125+
return err
126+
}
127+
128+
authconfig.Username = claims.Domain.Username
129+
authconfig.Password = res.AccessToken
130+
return nil
124131
}
125132

126133
var prompt string

cli/command/registry/login_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ func TestLoginTermination(t *testing.T) {
213213

214214
runErr := make(chan error)
215215
go func() {
216-
runErr <- runLogin(ctx, cli, loginOptions{})
216+
runErr <- runLogin(ctx, cli, loginOptions{
217+
user: "test-user",
218+
})
217219
}()
218220

219221
// Let the prompt get canceled by the context

cli/command/registry/logout.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) e
6161
}
6262
}
6363

64+
_ = dockerCli.OAuthManager().Logout()
65+
6466
// if at least one removal succeeded, report success. Otherwise report errors
6567
if len(errs) == len(regsToLogout) {
6668
fmt.Fprintln(dockerCli.Err(), "WARNING: could not erase credentials:")

cli/internal/oauth/api/api.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
"strings"
10+
"time"
11+
12+
"github.com/docker/cli/cli/internal/oauth/util"
13+
)
14+
15+
type OAuthAPI interface {
16+
GetDeviceCode(audience string) (State, error)
17+
WaitForDeviceToken(state State) (TokenResponse, error)
18+
Refresh(token string) (TokenResponse, error)
19+
LogoutURL() string
20+
}
21+
22+
// API represents API interactions with Auth0.
23+
type API struct {
24+
// BaseURL is the base used for each request to Auth0.
25+
BaseURL string
26+
// ClientID is the client ID for the application to auth with the tenant.
27+
ClientID string
28+
// Scopes are the scopes that are requested during the device auth flow.
29+
Scopes []string
30+
// Client is the client that is used for calls.
31+
Client util.Client
32+
}
33+
34+
// TokenResponse represents the response of the /oauth/token route.
35+
type TokenResponse struct {
36+
AccessToken string `json:"access_token"`
37+
IDToken string `json:"id_token"`
38+
RefreshToken string `json:"refresh_token"`
39+
Scope string `json:"scope"`
40+
ExpiresIn int `json:"expires_in"`
41+
TokenType string `json:"token_type"`
42+
Error *string `json:"error,omitempty"`
43+
ErrorDescription string `json:"error_description,omitempty"`
44+
}
45+
46+
var ErrTimeout = errors.New("timed out waiting for device token")
47+
48+
// GetDeviceCode returns device code authorization information from Auth0.
49+
func (a API) GetDeviceCode(audience string) (state State, err error) {
50+
data := url.Values{
51+
"client_id": {a.ClientID},
52+
"audience": {audience},
53+
"scope": {strings.Join(a.Scopes, " ")},
54+
}
55+
56+
deviceCodeURL := a.BaseURL + "/oauth/device/code"
57+
resp, err := a.Client.PostForm(deviceCodeURL, strings.NewReader(data.Encode()))
58+
if err != nil {
59+
return
60+
}
61+
defer func() {
62+
_ = resp.Body.Close()
63+
}()
64+
65+
if resp.StatusCode != http.StatusOK {
66+
var body map[string]any
67+
err = json.NewDecoder(resp.Body).Decode(&body)
68+
if errorDescription, ok := body["error_description"].(string); ok {
69+
return state, errors.New(errorDescription)
70+
}
71+
return state, fmt.Errorf("failed to get device code: %w", err)
72+
}
73+
74+
err = json.NewDecoder(resp.Body).Decode(&state)
75+
76+
return
77+
}
78+
79+
// WaitForDeviceToken polls to get tokens based on the device code set up. This
80+
// only works in a device auth flow.
81+
func (a API) WaitForDeviceToken(state State) (TokenResponse, error) {
82+
ticker := time.NewTicker(state.IntervalDuration())
83+
timeout := time.After(time.Duration(state.ExpiresIn) * time.Second)
84+
85+
for {
86+
select {
87+
case <-ticker.C:
88+
res, err := a.getDeviceToken(state)
89+
if err != nil {
90+
return res, err
91+
}
92+
93+
if res.Error != nil {
94+
if *res.Error == "authorization_pending" {
95+
continue
96+
}
97+
98+
return res, errors.New(res.ErrorDescription)
99+
}
100+
101+
return res, nil
102+
case <-timeout:
103+
ticker.Stop()
104+
return TokenResponse{}, ErrTimeout
105+
}
106+
}
107+
}
108+
109+
// getToken calls the token endpoint of Auth0 and returns the response.
110+
func (a API) getDeviceToken(state State) (res TokenResponse, err error) {
111+
data := url.Values{
112+
"client_id": {a.ClientID},
113+
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
114+
"device_code": {state.DeviceCode},
115+
}
116+
oauthTokenURL := a.BaseURL + "/oauth/token"
117+
118+
resp, err := a.Client.PostForm(oauthTokenURL, strings.NewReader(data.Encode()))
119+
if err != nil {
120+
return res, fmt.Errorf("failed to get code: %w", err)
121+
}
122+
123+
err = json.NewDecoder(resp.Body).Decode(&res)
124+
_ = resp.Body.Close()
125+
126+
return
127+
}
128+
129+
// Refresh returns new tokens based on the refresh token.
130+
func (a API) Refresh(token string) (res TokenResponse, err error) {
131+
data := url.Values{
132+
"grant_type": {"refresh_token"},
133+
"client_id": {a.ClientID},
134+
"refresh_token": {token},
135+
}
136+
137+
refreshURL := a.BaseURL + "/oauth/token"
138+
//nolint:gosec // Ignore G107: Potential HTTP request made with variable url
139+
resp, err := http.PostForm(refreshURL, data)
140+
if err != nil {
141+
return
142+
}
143+
144+
err = json.NewDecoder(resp.Body).Decode(&res)
145+
_ = resp.Body.Close()
146+
147+
return
148+
}
149+
150+
func (a API) LogoutURL() string {
151+
return fmt.Sprintf("%s/v2/logout?client_id=%s", a.BaseURL, a.ClientID)
152+
}

0 commit comments

Comments
 (0)