Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying bearerToken for git http token authentication #442

Merged
merged 8 commits into from
Jan 20, 2023
19 changes: 16 additions & 3 deletions git/gogit/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ func (g *Client) validateUrl(u string) error {
return fmt.Errorf("cannot parse url: %w", err)
}

if g.authOpts != nil {
httpOrHttps := g.authOpts.Transport == git.HTTP || g.authOpts.Transport == git.HTTPS
hasUsernameOrPassword := g.authOpts.Username != "" || g.authOpts.Password != ""
hasBearerToken := g.authOpts.BearerToken != ""

if httpOrHttps && hasBearerToken && hasUsernameOrPassword {
return errors.New("basic auth and bearer token cannot be set at the same time")
}
}

if g.credentialsOverHTTP {
return nil
}
Expand All @@ -254,9 +264,12 @@ func (g *Client) validateUrl(u string) error {
return errors.New("URL cannot contain credentials when using HTTP")
}

if httpOrEmpty && g.authOpts != nil &&
(g.authOpts.Username != "" || g.authOpts.Password != "") {
return errors.New("basic auth cannot be sent over HTTP")
if httpOrEmpty && g.authOpts != nil {
if g.authOpts.Username != "" || g.authOpts.Password != "" {
return errors.New("basic auth cannot be sent over HTTP")
} else if g.authOpts.BearerToken != "" {
return errors.New("bearer token cannot be sent over HTTP")
}
darkowlzz marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
Expand Down
103 changes: 103 additions & 0 deletions git/gogit/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,3 +653,106 @@ func TestHead(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(hash.String()).To(Equal(cc))
}

func TestValidateUrl(t *testing.T) {
tests := []struct {
name string
transport git.TransportType
username string
password string
bearerToken string
url string
credentialsOverHttp bool
expectedError string
}{
{
name: "blocked: basic auth over http",
transport: git.HTTP,
username: "user",
password: "pass",
url: "http://url",
expectedError: "basic auth cannot be sent over HTTP",
},
{
name: "allowed: basic auth over http with insecure enabled",
transport: git.HTTP,
username: "user",
password: "pass",
url: "http://url",
credentialsOverHttp: true,
},
{
name: "allowed: basic auth over https",
transport: git.HTTPS,
username: "user",
password: "pass",
url: "https://url",
},
{
name: "blocked: bearer token over http",
transport: git.HTTP,
bearerToken: "token",
url: "http://url",
expectedError: "bearer token cannot be sent over HTTP",
},
{
name: "allowed: bearer token over http with insecure enabled",
transport: git.HTTP,
bearerToken: "token",
url: "http://url",
credentialsOverHttp: true,
},
{
name: "allowed: bearer token over https",
transport: git.HTTPS,
bearerToken: "token",
url: "https://url",
},
{
name: "blocked: basic auth and bearer token at the same time over http",
transport: git.HTTP,
username: "user",
password: "pass",
bearerToken: "token",
url: "http://url",
expectedError: "basic auth and bearer token cannot be set at the same time",
},
{
name: "blocked: basic auth and bearer token at the same time over https",
transport: git.HTTPS,
username: "user",
password: "pass",
bearerToken: "token",
url: "https://url",
expectedError: "basic auth and bearer token cannot be set at the same time",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

opts := []ClientOption{WithDiskStorage()}
if tt.credentialsOverHttp {
opts = append(opts, WithInsecureCredentialsOverHTTP())
}

ggc, err := NewClient(t.TempDir(), &git.AuthOptions{
Transport: tt.transport,
Username: tt.username,
Password: tt.password,
BearerToken: tt.bearerToken,
}, opts...)
g.Expect(err).ToNot(HaveOccurred())

err = ggc.validateUrl(tt.url)

if tt.expectedError == "" {
g.Expect(err).To(BeNil())
} else {
g.Expect(err).ToNot(BeNil())
g.Expect(err.Error()).To(ContainSubstring(tt.expectedError))
}
})
}
}
20 changes: 17 additions & 3 deletions git/gogit/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,7 @@ func TestClone_CredentialsOverHttp(t *testing.T) {
name string
username string
password string
bearerToken string
allowCredentialsOverHttp bool
transformURL func(string) string
expectCloneErr string
Expand All @@ -1009,6 +1010,11 @@ func TestClone_CredentialsOverHttp(t *testing.T) {
password: "pass",
expectCloneErr: "basic auth cannot be sent over HTTP",
},
{
name: "blocked: bearer token over HTTP",
bearerToken: "token",
expectCloneErr: "bearer token cannot be sent over HTTP",
},
{
name: "blocked: URL based credential over HTTP (name)",
transformURL: func(s string) string {
Expand Down Expand Up @@ -1069,6 +1075,13 @@ func TestClone_CredentialsOverHttp(t *testing.T) {
allowCredentialsOverHttp: true,
expectRequest: true,
},
{
name: "allowed: bearer token over HTTP",
bearerToken: "token",
expectCloneErr: "unable to clone",
allowCredentialsOverHttp: true,
expectRequest: true,
},
{
name: "allowed: URL based credential over HTTP (name)",
transformURL: func(s string) string {
Expand Down Expand Up @@ -1129,9 +1142,10 @@ func TestClone_CredentialsOverHttp(t *testing.T) {
}

ggc, err := NewClient(tmpDir, &git.AuthOptions{
Transport: git.HTTP,
Username: tt.username,
Password: tt.password,
Transport: git.HTTP,
Username: tt.username,
Password: tt.password,
BearerToken: tt.bearerToken,
}, opts...)

g.Expect(err).ToNot(HaveOccurred())
Expand Down
4 changes: 4 additions & 0 deletions git/gogit/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func transportAuth(opts *git.AuthOptions, fallbackToDefaultKnownHosts bool) (tra
Username: opts.Username,
Password: opts.Password,
}, nil
} else if opts.BearerToken != "" {
return &http.TokenAuth{
Token: opts.BearerToken,
}, nil
}
return nil, nil
case git.SSH:
Expand Down
24 changes: 24 additions & 0 deletions git/gogit/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ func Test_transportAuth(t *testing.T) {
}))
},
},
{
name: "HTTP bearer token",
opts: &git.AuthOptions{
Transport: git.HTTP,
BearerToken: "http-token",
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
g.Expect(t).To(Equal(&http.TokenAuth{
Token: opts.BearerToken,
}))
},
},
{
name: "HTTPS basic auth",
opts: &git.AuthOptions{
Expand All @@ -125,6 +137,18 @@ func Test_transportAuth(t *testing.T) {
}))
},
},
{
name: "HTTPS bearer token",
opts: &git.AuthOptions{
Transport: git.HTTPS,
BearerToken: "https-token",
},
wantFunc: func(g *WithT, t transport.AuthMethod, opts *git.AuthOptions) {
g.Expect(t).To(Equal(&http.TokenAuth{
Token: opts.BearerToken,
}))
},
},
{
name: "SSH private key",
opts: &git.AuthOptions{
Expand Down
49 changes: 29 additions & 20 deletions git/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ const (
// AuthOptions are the authentication options for the Transport of
// communication with a remote origin.
type AuthOptions struct {
Transport TransportType
Host string
Username string
Password string
Identity []byte
KnownHosts []byte
CAFile []byte
Transport TransportType
Host string
Username string
Password string
BearerToken string
Identity []byte
KnownHosts []byte
CAFile []byte
}

// KexAlgos hosts the key exchange algorithms to be used for SSH connections.
Expand Down Expand Up @@ -86,24 +87,32 @@ func (o AuthOptions) Validate() error {
func NewAuthOptions(u url.URL, data map[string][]byte) (*AuthOptions, error) {
opts := newAuthOptions(u)
if len(data) > 0 {
opts.Username = string(data["username"])
opts.Password = string(data["password"])
opts.CAFile = data["caFile"]
opts.Identity = data["identity"]
opts.KnownHosts = data["known_hosts"]
if opts.Transport == SSH {
opts.Identity = data["identity"]
opts.KnownHosts = data["known_hosts"]
opts.Username = u.User.Username()
opts.Password = string(data["password"])
// We fallback to using "git" as the username when cloning Git
// repositories through SSH since that's the conventional username used
// by Git providers.
if opts.Username == "" {
opts.Username = DefaultPublicKeyAuthUser
}
} else if token, found := data["bearerToken"]; found {
opts.CAFile = data["caFile"]
opts.BearerToken = string(token)
} else {
opts.CAFile = data["caFile"]
opts.Username = string(data["username"])
opts.Password = string(data["password"])
}
}

if opts.Username == "" {
if opts.Transport != SSH && opts.Username == "" {
opts.Username = u.User.Username()
}

// We fallback to using "git" as the username when cloning Git
// repositories through SSH since that's the conventional username used
// by Git providers.
if opts.Username == "" && opts.Transport == SSH {
opts.Username = DefaultPublicKeyAuthUser
}
if opts.Password == "" {
if opts.Transport != SSH && opts.Password == "" {
opts.Password, _ = u.User.Password()
}

Expand Down
Loading