Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Laura Brehm <[email protected]>
  • Loading branch information
laurazard committed Jul 4, 2024
1 parent 404bf26 commit ed4e811
Show file tree
Hide file tree
Showing 56 changed files with 11,366 additions and 12 deletions.
16 changes: 16 additions & 0 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/internal/oauth/manager"
manifeststore "github.com/docker/cli/cli/manifest/store"
"github.com/docker/cli/cli/oauth"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
Expand All @@ -32,6 +34,7 @@ import (
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
dregistry "github.com/docker/docker/registry"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -66,6 +69,7 @@ type Cli interface {
CurrentContext() string
DockerEndpoint() docker.Endpoint
TelemetryClient
OAuthManager() oauth.Manager
}

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

enableGlobalMeter, enableGlobalTracer bool

oauthManager *manager.OAuthManager
}

// DefaultVersion returns api.defaultVersion.
Expand Down Expand Up @@ -254,6 +260,10 @@ func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClien
}
}

func (cli *DockerCli) OAuthManager() oauth.Manager {
return cli.oauthManager
}

// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
Expand Down Expand Up @@ -293,6 +303,12 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
cli.createGlobalTracerProvider(cli.baseCtx)
}

oauthManager, err := manager.NewManager(cli.ConfigFile().GetCredentialsStore(dregistry.IndexServer))
if err != nil {
return err
}
cli.oauthManager = oauthManager

return nil
}

Expand Down
25 changes: 16 additions & 9 deletions cli/command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,14 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/credentials"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/hints"
"github.com/docker/cli/cli/internal/oauth/util"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
)

const patSuggest = "You can log in with your password or a Personal Access " +
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"

// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
Expand Down Expand Up @@ -87,6 +83,8 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
}

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

Expand All @@ -117,10 +115,19 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
if isDefaultRegistry {
// if this is a default registry (docker hub), then display the following message.
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.")
if hints.Enabled() {
fmt.Fprintln(cli.Out(), patSuggest)
fmt.Fprintln(cli.Out())

res, err := cli.OAuthManager().LoginDevice(ctx, cli.Err())
if err != nil {
return err
}
claims, err := util.GetClaims(res.AccessToken)
if err != nil {
return err
}

authconfig.Username = claims.Domain.Username
authconfig.Password = res.AccessToken
return nil
}

var prompt string
Expand Down
4 changes: 3 additions & 1 deletion cli/command/registry/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ func TestLoginTermination(t *testing.T) {

runErr := make(chan error)
go func() {
runErr <- runLogin(ctx, cli, loginOptions{})
runErr <- runLogin(ctx, cli, loginOptions{
user: "test-user",
})
}()

// Let the prompt get canceled by the context
Expand Down
2 changes: 2 additions & 0 deletions cli/command/registry/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func runLogout(_ context.Context, dockerCli command.Cli, serverAddress string) e
}
}

_ = dockerCli.OAuthManager().Logout()

// if at least one removal succeeded, report success. Otherwise report errors
if len(errs) == len(regsToLogout) {
fmt.Fprintln(dockerCli.Err(), "WARNING: could not erase credentials:")
Expand Down
152 changes: 152 additions & 0 deletions cli/internal/oauth/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package api

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/docker/cli/cli/internal/oauth/util"
)

type OAuthAPI interface {
GetDeviceCode(audience string) (State, error)
WaitForDeviceToken(state State) (TokenResponse, error)
Refresh(token string) (TokenResponse, error)
LogoutURL() string
}

// API represents API interactions with Auth0.
type API struct {
// BaseURL is the base used for each request to Auth0.
BaseURL string
// ClientID is the client ID for the application to auth with the tenant.
ClientID string
// Scopes are the scopes that are requested during the device auth flow.
Scopes []string
// Client is the client that is used for calls.
Client util.Client
}

// TokenResponse represents the response of the /oauth/token route.
type TokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Error *string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
}

var ErrTimeout = errors.New("timed out waiting for device token")

// GetDeviceCode returns device code authorization information from Auth0.
func (a API) GetDeviceCode(audience string) (state State, err error) {
data := url.Values{
"client_id": {a.ClientID},
"audience": {audience},
"scope": {strings.Join(a.Scopes, " ")},
}

deviceCodeURL := a.BaseURL + "/oauth/device/code"
resp, err := a.Client.PostForm(deviceCodeURL, strings.NewReader(data.Encode()))
if err != nil {
return
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
var body map[string]any
err = json.NewDecoder(resp.Body).Decode(&body)
if errorDescription, ok := body["error_description"].(string); ok {
return state, errors.New(errorDescription)
}
return state, fmt.Errorf("failed to get device code: %w", err)
}

err = json.NewDecoder(resp.Body).Decode(&state)

return
}

// WaitForDeviceToken polls to get tokens based on the device code set up. This
// only works in a device auth flow.
func (a API) WaitForDeviceToken(state State) (TokenResponse, error) {
ticker := time.NewTicker(state.IntervalDuration())
timeout := time.After(time.Duration(state.ExpiresIn) * time.Second)

for {
select {
case <-ticker.C:
res, err := a.getDeviceToken(state)
if err != nil {
return res, err
}

if res.Error != nil {
if *res.Error == "authorization_pending" {
continue
}

return res, errors.New(res.ErrorDescription)
}

return res, nil
case <-timeout:
ticker.Stop()
return TokenResponse{}, ErrTimeout
}
}
}

// getToken calls the token endpoint of Auth0 and returns the response.
func (a API) getDeviceToken(state State) (res TokenResponse, err error) {
data := url.Values{
"client_id": {a.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {state.DeviceCode},
}
oauthTokenURL := a.BaseURL + "/oauth/token"

resp, err := a.Client.PostForm(oauthTokenURL, strings.NewReader(data.Encode()))
if err != nil {
return res, fmt.Errorf("failed to get code: %w", err)
}

err = json.NewDecoder(resp.Body).Decode(&res)
_ = resp.Body.Close()

return
}

// Refresh returns new tokens based on the refresh token.
func (a API) Refresh(token string) (res TokenResponse, err error) {
data := url.Values{
"grant_type": {"refresh_token"},
"client_id": {a.ClientID},
"refresh_token": {token},
}

refreshURL := a.BaseURL + "/oauth/token"
//nolint:gosec // Ignore G107: Potential HTTP request made with variable url
resp, err := http.PostForm(refreshURL, data)
if err != nil {
return
}

err = json.NewDecoder(resp.Body).Decode(&res)
_ = resp.Body.Close()

return
}

func (a API) LogoutURL() string {
return fmt.Sprintf("%s/v2/logout?client_id=%s", a.BaseURL, a.ClientID)
}
Loading

0 comments on commit ed4e811

Please sign in to comment.