Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 45 additions & 0 deletions cmd/harbor/root/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/goharbor/go-client/pkg/harbor"
"github.com/goharbor/go-client/pkg/sdk/v2.0/client"
Expand All @@ -38,6 +39,7 @@ var (
Name string
passwordStdin bool
skipVerifyClient bool
oidcLogin bool
)

// LoginCommand creates a new `harbor login` command
Expand All @@ -52,6 +54,10 @@ func LoginCommand() *cobra.Command {
serverAddress = args[0]
}

if oidcLogin {
return RunOIDCLogin(serverAddress)
}

if passwordStdin {
fmt.Print("Password: ")
passwordBytes, err := term.ReadPassword(int(os.Stdin.Fd())) // #nosec G115 - fd fits in int on all supported platforms
Expand Down Expand Up @@ -87,9 +93,13 @@ func LoginCommand() *cobra.Command {
flags.StringVarP(&Name, "context-name", "n", "", "Login context name (optional)")
flags.StringVarP(&Password, "password", "p", "", "Password (not recommended, use --password-stdin for better security)")
flags.BoolVar(&passwordStdin, "password-stdin", false, "Take the password from stdin")
flags.BoolVar(&oidcLogin, "oidc", false, "Authenticate using Harbor OIDC browser login")
flags.BoolVarP(&skipVerifyClient, "skip-verify-client", "", false, "Skip whether the clients basic auth credentials shall be validated against the Harbor server during login. This is not recommended as it may lead to storing invalid credentials. Use this flag if you want to skip validation of credentials during login, for example, when the Harbor server is not reachable at the moment of login but you still want to store the credentials for later use.")

cmd.MarkFlagsMutuallyExclusive("password", "password-stdin")
cmd.MarkFlagsMutuallyExclusive("oidc", "username")
cmd.MarkFlagsMutuallyExclusive("oidc", "password")
cmd.MarkFlagsMutuallyExclusive("oidc", "password-stdin")

return cmd
}
Expand Down Expand Up @@ -208,6 +218,41 @@ func RunLogin(opts login.LoginView) error {
return nil
}

func RunOIDCLogin(serverAddress string) error {
if serverAddress == "" {
return fmt.Errorf("server address is required for OIDC login")
}
serverAddress = utils.FormatUrl(serverAddress)
if err := utils.ValidateURL(serverAddress); err != nil {
return fmt.Errorf("invalid server URL: %w", err)
}

loginResp, err := utils.InitiateOIDCLogin(serverAddress)
if err != nil {
return err
}

fmt.Printf("Open this URL in your browser to authenticate:\n\n %s\n\n", loginResp.RedirectURL)
fmt.Print("Waiting for authentication...\n")

tokenResp, err := utils.PollForOIDCToken(serverAddress, loginResp.PollToken, 10*time.Minute)
if err != nil {
return err
}

harborData, err := utils.GetCurrentHarborData()
if err != nil {
return fmt.Errorf("failed to get current harbor data: %s", err)
}

if err := utils.AddOIDCCredentials(serverAddress, tokenResp.Username, tokenResp.IDToken, tokenResp.RefreshToken, tokenResp.ExpiresAt, harborData.ConfigPath); err != nil {
return fmt.Errorf("failed to store OIDC credential: %w", err)
}

fmt.Printf("Login successful for %s at %s\n", tokenResp.Username, serverAddress)
return nil
}

func validateClientConnection(client *client.HarborAPI) error {
ctx := context.Background()

Expand Down
59 changes: 59 additions & 0 deletions cmd/harbor/root/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
package root_test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/goharbor/harbor-cli/cmd/harbor/root"
"github.com/goharbor/harbor-cli/pkg/utils"
helpers "github.com/goharbor/harbor-cli/test/helper"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -122,3 +126,58 @@ func Test_Login_Failure_MutuallyExclusiveFlags(t *testing.T) {
err := cmd.Execute()
assert.Error(t, err, "Expected error when both --password and --password-stdin are set")
}

func Test_Login_Failure_OIDCMutuallyExclusiveFlags(t *testing.T) {
tempDir := t.TempDir()
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)

cmd := root.LoginCommand()
cmd.SetArgs([]string{"http://demo.goharbor.io"})

assert.NoError(t, cmd.Flags().Set("oidc", "true"))
assert.NoError(t, cmd.Flags().Set("username", "admin"))

err := cmd.Execute()
assert.Error(t, err, "Expected error when --oidc and --username are set")
}

func Test_RunOIDCLogin_Failure_MissingServer(t *testing.T) {
err := root.RunOIDCLogin("")
assert.Error(t, err)
}

func Test_RunOIDCLogin_Success(t *testing.T) {
tempDir := t.TempDir()
helpers.SetMockKeyring(t)
data := helpers.Initialize(t, tempDir)
defer helpers.ConfigCleanup(t, data)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/c/oidc/login":
assert.Equal(t, "cli", r.URL.Query().Get("mode"))
assert.NoError(t, json.NewEncoder(w).Encode(utils.OIDCLoginResponse{
RedirectURL: "https://idp.example/authorize",
PollToken: "poll-token-1",
}))
case "/c/oidc/cli-token":
assert.Equal(t, "poll-token-1", r.URL.Query().Get("poll_token"))
assert.NoError(t, json.NewEncoder(w).Encode(utils.OIDCPollResponse{
Status: "ready",
IDToken: "id-token",
Username: "alice",
}))
default:
http.NotFound(w, r)
}
}))
defer server.Close()

err := root.RunOIDCLogin(server.URL)
assert.NoError(t, err)

cred, err := utils.GetCredentials(utils.DefaultCredentialName("alice", server.URL))
assert.NoError(t, err)
assert.Equal(t, utils.AuthTypeOIDC, cred.AuthType)
}
1 change: 1 addition & 0 deletions doc/cli-docs/harbor-login.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ harbor login [server] [flags]
```sh
-n, --context-name string Login context name (optional)
-h, --help help for login
--oidc Authenticate using Harbor OIDC browser login
-p, --password string Password (not recommended, use --password-stdin for better security)
--password-stdin Take the password from stdin
--skip-verify-client Skip whether the clients basic auth credentials shall be validated against the Harbor server during login. This is not recommended as it may lead to storing invalid credentials. Use this flag if you want to skip validation of credentials during login, for example, when the Harbor server is not reachable at the moment of login but you still want to store the credentials for later use.
Expand Down
4 changes: 4 additions & 0 deletions doc/man-docs/man1/harbor-login.1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Authenticate with Harbor Registry.
\fB-h\fP, \fB--help\fP[=false]
help for login

.PP
\fB--oidc\fP[=false]
Authenticate using Harbor OIDC browser login

.PP
\fB-p\fP, \fB--password\fP=""
Password (not recommended, use --password-stdin for better security)
Expand Down
Loading