Skip to content

Commit 1923cf3

Browse files
committed
auth: refactor git credentials
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
1 parent f595d79 commit 1923cf3

9 files changed

Lines changed: 825 additions & 129 deletions

File tree

auth/aws/provider.go

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,20 @@ import (
4545

4646
// ProviderName is the name of the AWS authentication provider.
4747
const (
48-
ProviderName = "aws"
48+
ProviderName = "aws"
49+
50+
// codeCommitCanonicalTimestampFormat is the SigV4 canonical timestamp
51+
// format (ISO 8601 basic, no separators, no fractional seconds) embedded
52+
// in the CodeCommit Git password and used as the canonical request time
53+
// during signing. The trailing 'Z' is appended separately by the caller.
4954
codeCommitCanonicalTimestampFormat = "20060102T150405"
55+
56+
// codeCommitSignatureValidity is the server-side replay window for
57+
// SigV4-signed CodeCommit Git requests. AWS documents that the password
58+
// generated for HTTPS access to a CodeCommit repository stops working
59+
// after about 15 minutes:
60+
// https://docs.aws.amazon.com/codecommit/latest/userguide/troubleshooting-ch.html
61+
codeCommitSignatureValidity = 15 * time.Minute
5062
)
5163

5264
// Provider implements the auth.Provider interface for AWS authentication.
@@ -398,24 +410,11 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
398410
}, nil
399411
}
400412

401-
func (p Provider) impl() Implementation {
402-
if p.Implementation == nil {
403-
return implementation{}
404-
}
405-
return p.Implementation
406-
}
407-
408-
type signerHeaderHostOnly struct{}
409-
410-
func (signerHeaderHostOnly) IsSigned(h string) bool {
411-
return h == "host"
412-
}
413-
414-
// GetRegionFromCodeCommitURL extracts the AWS region from a CodeCommit HTTPS
413+
// getRegionFromCodeCommitURL extracts the AWS region from a CodeCommit HTTPS
415414
// git URL (e.g. https://git-codecommit.us-east-1.amazonaws.com/...).
416415
// Returns an error if the URL is nil, not HTTPS, or not a valid CodeCommit URL.
417416
// https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-git
418-
func GetRegionFromCodeCommitURL(gitURL *url.URL) (string, error) {
417+
func getRegionFromCodeCommitURL(gitURL *url.URL) (string, error) {
419418
if gitURL == nil {
420419
return "", fmt.Errorf("Git URL must be specified for AWS CodeCommit authentication")
421420
}
@@ -431,28 +430,51 @@ func GetRegionFromCodeCommitURL(gitURL *url.URL) (string, error) {
431430
return urlSplit[1], nil
432431
}
433432

434-
// NewCodeCommitGitToken returns HTTPS Git credentials for AWS CodeCommit.
435-
func (Provider) NewCodeCommitGitCredentials(_ context.Context, accessTokens []auth.Token, opts ...auth.Option) (string, string, error) {
436-
var o auth.Options
437-
o.Apply(opts...)
433+
// GetAccessTokenOptionsForGitRepository implements auth.GitCredentialsProvider.
434+
// AWS requires a region for obtaining access credentials. To avoid requiring
435+
// callers to pass a region in addition to the CodeCommit URL, we extract the
436+
// region from the URL and inject it as STSRegion so that object-level workload
437+
// identity (which requires an explicit region) works without extra config.
438+
func (Provider) GetAccessTokenOptionsForGitRepository(gitURL *url.URL) ([]auth.Option, error) {
439+
region, err := getRegionFromCodeCommitURL(gitURL)
440+
if err != nil {
441+
return nil, err
442+
}
443+
return []auth.Option{auth.WithSTSRegion(region)}, nil
444+
}
438445

439-
gitURL := o.GitURL
440-
region, err := GetRegionFromCodeCommitURL(gitURL)
446+
// ParseGitRepository implements auth.GitCredentialsProvider.
447+
// It validates the URL is a CodeCommit URL and returns the URL string so that
448+
// it is included in the cache key: CodeCommit credentials are a SigV4 signature
449+
// over the request URL, so distinct URLs must map to distinct cache entries.
450+
func (Provider) ParseGitRepository(gitURL *url.URL) (string, error) {
451+
if _, err := getRegionFromCodeCommitURL(gitURL); err != nil {
452+
return "", err
453+
}
454+
return gitURL.String(), nil
455+
}
456+
457+
// NewGitCredentials implements auth.GitCredentialsProvider.
458+
func (Provider) NewGitCredentials(_ context.Context, gitInput string,
459+
accessToken auth.Token, _ ...auth.Option) (*auth.GitCredentials, error) {
460+
461+
gitURL, err := url.Parse(gitInput)
441462
if err != nil {
442-
return "", "", err
463+
return nil, fmt.Errorf("failed to parse CodeCommit URL: %w", err)
443464
}
444-
if len(accessTokens) == 0 {
445-
return "", "", fmt.Errorf("AWS access token is required for region %q", region)
465+
region, err := getRegionFromCodeCommitURL(gitURL)
466+
if err != nil {
467+
return nil, err
446468
}
447469

448-
creds, ok := accessTokens[0].(*Credentials)
470+
creds, ok := accessToken.(*Credentials)
449471
if !ok {
450-
return "", "", fmt.Errorf("failed to cast token to AWS token: %T", accessTokens[0])
472+
return nil, fmt.Errorf("failed to cast token to AWS token: %T", accessToken)
451473
}
452474

453475
req, err := http.NewRequest("GIT", gitURL.String(), nil)
454476
if err != nil {
455-
return "", "", fmt.Errorf("failed to build CodeCommit signing request: %w", err)
477+
return nil, fmt.Errorf("failed to build CodeCommit signing request: %w", err)
456478
}
457479
req.Host = gitURL.Host
458480

@@ -477,15 +499,39 @@ func (Provider) NewCodeCommitGitCredentials(_ context.Context, accessTokens []au
477499
}
478500

479501
if err := signer.SignRequest(signInput); err != nil {
480-
return "", "", fmt.Errorf("failed to sign request: %w", err)
502+
return nil, fmt.Errorf("failed to sign request: %w", err)
481503
}
482504

483505
authHeader := req.Header.Get("Authorization")
484-
sigStart := strings.Index(authHeader, "Signature=")
485-
signature := authHeader[sigStart+10:]
506+
_, after, _ := strings.Cut(authHeader, "Signature=")
507+
signature := after
486508

487509
username := strings.Join([]string{*creds.AccessKeyId, *creds.SessionToken}, "%")
488510
password := signingTime.Format(codeCommitCanonicalTimestampFormat) + "Z" + signature
489511

490-
return username, password, nil
512+
// The signed password is invalid once either the server's replay window
513+
// elapses or the underlying credentials expire, whichever comes first.
514+
expiresAt := signingTime.Add(codeCommitSignatureValidity)
515+
if creds.Expiration.Before(expiresAt) {
516+
expiresAt = *creds.Expiration
517+
}
518+
519+
return &auth.GitCredentials{
520+
Username: username,
521+
Password: password,
522+
ExpiresAt: expiresAt,
523+
}, nil
524+
}
525+
526+
func (p Provider) impl() Implementation {
527+
if p.Implementation == nil {
528+
return implementation{}
529+
}
530+
return p.Implementation
531+
}
532+
533+
type signerHeaderHostOnly struct{}
534+
535+
func (signerHeaderHostOnly) IsSigned(h string) bool {
536+
return h == "host"
491537
}

auth/aws/provider_test.go

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
538538
g.Expect(o.STSRegion).To(Equal("us-west-2"))
539539
}
540540

541-
func TestGetRegionFromCodeCommitURL(t *testing.T) {
541+
func TestProvider_GetAccessTokenOptionsForGitRepository(t *testing.T) {
542542
for _, tt := range []struct {
543543
name string
544544
gitURL string
@@ -583,27 +583,88 @@ func TestGetRegionFromCodeCommitURL(t *testing.T) {
583583
parsedURL, err = url.Parse(tt.gitURL)
584584
g.Expect(err).NotTo(HaveOccurred())
585585
}
586-
region, err := aws.GetRegionFromCodeCommitURL(parsedURL)
586+
atOpts, err := aws.Provider{}.GetAccessTokenOptionsForGitRepository(parsedURL)
587587
if tt.err != "" {
588588
g.Expect(err).To(HaveOccurred())
589589
g.Expect(err.Error()).To(Equal(tt.err))
590+
g.Expect(atOpts).To(BeNil())
590591
} else {
591592
g.Expect(err).NotTo(HaveOccurred())
592-
g.Expect(region).To(Equal(tt.expectedRegion))
593+
var o auth.Options
594+
o.Apply(atOpts...)
595+
g.Expect(o.STSRegion).To(Equal(tt.expectedRegion))
593596
}
594597
})
595598
}
596599
}
597600

598-
func TestProvider_NewCodeCommitGitCredentials(t *testing.T) {
601+
func TestProvider_ParseGitRepository(t *testing.T) {
602+
for _, tt := range []struct {
603+
name string
604+
gitURL string
605+
expectedRegion string
606+
err string
607+
}{
608+
{
609+
name: "valid CodeCommit URL",
610+
gitURL: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo",
611+
expectedRegion: "us-east-1",
612+
},
613+
{
614+
name: "valid CodeCommit FIPS URL",
615+
gitURL: "https://git-codecommit-fips.us-west-2.amazonaws.com/v1/repos/test-repo",
616+
expectedRegion: "us-west-2",
617+
},
618+
{
619+
name: "valid CodeCommit China URL",
620+
gitURL: "https://git-codecommit.cn-north-1.amazonaws.com.cn/v1/repos/test-repo",
621+
expectedRegion: "cn-north-1",
622+
},
623+
{
624+
name: "nil URL",
625+
err: "Git URL must be specified for AWS CodeCommit authentication",
626+
},
627+
{
628+
name: "non-HTTPS URL",
629+
gitURL: "http://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo",
630+
err: "AWS CodeCommit authentication requires an HTTPS Git URL",
631+
},
632+
{
633+
name: "invalid CodeCommit URL",
634+
gitURL: "https://github.com/org/repo",
635+
err: "invalid AWS CodeCommit Git URL: github.com",
636+
},
637+
} {
638+
t.Run(tt.name, func(t *testing.T) {
639+
g := NewWithT(t)
640+
var parsedURL *url.URL
641+
if tt.gitURL != "" {
642+
var err error
643+
parsedURL, err = url.Parse(tt.gitURL)
644+
g.Expect(err).NotTo(HaveOccurred())
645+
}
646+
gitInput, err := aws.Provider{}.ParseGitRepository(parsedURL)
647+
if tt.err != "" {
648+
g.Expect(err).To(HaveOccurred())
649+
g.Expect(err.Error()).To(Equal(tt.err))
650+
g.Expect(gitInput).To(BeEmpty())
651+
} else {
652+
g.Expect(err).NotTo(HaveOccurred())
653+
g.Expect(gitInput).To(Equal(tt.gitURL))
654+
}
655+
})
656+
}
657+
}
658+
659+
func TestProvider_NewGitCredentials(t *testing.T) {
599660
invalidToken := &generic.Token{Token: "invalid", ExpiresAt: time.Now().Add(time.Hour)}
600661
proxyUrl := url.URL{Scheme: "http", Host: "proxy.example.com"}
601662
awsRegion := "us-east-1"
602663
for _, tt := range []struct {
603664
name string
604665
gitURL string
605666
getAccessToken bool
606-
accessTokens []auth.Token
667+
accessToken auth.Token
607668
expectedUsername string
608669
err string
609670
}{
@@ -625,35 +686,11 @@ func TestProvider_NewCodeCommitGitCredentials(t *testing.T) {
625686
getAccessToken: true,
626687
expectedUsername: "access-key-id%session-token",
627688
},
628-
{
629-
name: "missing Git URL",
630-
getAccessToken: true,
631-
err: "Git URL must be specified for AWS CodeCommit authentication",
632-
},
633-
{
634-
name: "non HTTPS URL",
635-
gitURL: "http://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo",
636-
getAccessToken: true,
637-
err: "AWS CodeCommit authentication requires an HTTPS Git URL",
638-
},
639-
{
640-
name: "invalid CodeCommit URL",
641-
gitURL: "https://github.com/org/repo",
642-
getAccessToken: true,
643-
err: "invalid AWS CodeCommit Git URL: github.com",
644-
},
645-
{
646-
name: "missing access token",
647-
gitURL: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo",
648-
getAccessToken: false,
649-
accessTokens: []auth.Token{},
650-
err: `AWS access token is required for region "us-east-1"`,
651-
},
652689
{
653690
name: "invalid access token type",
654691
gitURL: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo",
655692
getAccessToken: false,
656-
accessTokens: []auth.Token{invalidToken},
693+
accessToken: invalidToken,
657694
err: "failed to cast token to AWS token: *generic.Token",
658695
},
659696
} {
@@ -667,35 +704,27 @@ func TestProvider_NewCodeCommitGitCredentials(t *testing.T) {
667704
returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id", SecretAccessKey: "secret-access-key", SessionToken: "session-token"},
668705
}
669706

670-
opts := []auth.Option{}
671-
if tt.gitURL != "" {
672-
gitURL, err := url.Parse(tt.gitURL)
673-
g.Expect(err).NotTo(HaveOccurred())
674-
opts = append(opts, auth.WithGitURL(*gitURL))
675-
}
676-
677707
provider := aws.Provider{Implementation: impl}
678-
accessTokens := tt.accessTokens
708+
accessToken := tt.accessToken
679709
if tt.getAccessToken {
680-
accessToken, err := auth.GetAccessToken(t.Context(), provider,
710+
var err error
711+
accessToken, err = auth.GetAccessToken(t.Context(), provider,
681712
auth.WithSTSRegion(awsRegion),
682713
auth.WithProxyURL(proxyUrl),
683714
)
684715
g.Expect(err).NotTo(HaveOccurred())
685-
accessTokens = []auth.Token{accessToken}
686716
}
687717

688-
username, password, err := provider.NewCodeCommitGitCredentials(t.Context(), accessTokens, opts...)
718+
creds, err := provider.NewGitCredentials(t.Context(), tt.gitURL, accessToken)
689719

690720
if tt.err == "" {
691721
g.Expect(err).NotTo(HaveOccurred())
692-
g.Expect(username).To(Equal(tt.expectedUsername))
693-
g.Expect(password).To(MatchRegexp(`^[0-9]{8}T[0-9]{6}Z[0-9a-f]{64}$`))
722+
g.Expect(creds.Username).To(Equal(tt.expectedUsername))
723+
g.Expect(creds.Password).To(MatchRegexp(`^[0-9]{8}T[0-9]{6}Z[0-9a-f]{64}$`))
694724
} else {
695725
g.Expect(err).To(HaveOccurred())
696726
g.Expect(err.Error()).To(Equal(tt.err))
697-
g.Expect(username).To(BeEmpty())
698-
g.Expect(password).To(BeEmpty())
727+
g.Expect(creds).To(BeNil())
699728
}
700729
})
701730
}

auth/azure/provider.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package azure
1919
import (
2020
"context"
2121
"fmt"
22+
"net/url"
2223
"os"
2324
"regexp"
2425
"strings"
@@ -394,6 +395,34 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
394395
}, nil
395396
}
396397

398+
// GetAccessTokenOptionsForGitRepository implements auth.GitCredentialsProvider.
399+
// Azure DevOps requires the DevOps scope for obtaining an access token that
400+
// can be used as a bearer token against Azure Repos.
401+
func (Provider) GetAccessTokenOptionsForGitRepository(*url.URL) ([]auth.Option, error) {
402+
return []auth.Option{auth.WithScopes(ScopeDevOps)}, nil
403+
}
404+
405+
// ParseGitRepository implements auth.GitCredentialsProvider.
406+
// Azure DevOps access tokens are not bound to the Git repository URL,
407+
// so this returns a constant input that does not affect the cache key.
408+
func (Provider) ParseGitRepository(*url.URL) (string, error) {
409+
return "azure-devops", nil
410+
}
411+
412+
// NewGitCredentials implements auth.GitCredentialsProvider.
413+
func (Provider) NewGitCredentials(_ context.Context, _ string,
414+
accessToken auth.Token, _ ...auth.Option) (*auth.GitCredentials, error) {
415+
416+
token, ok := accessToken.(*Token)
417+
if !ok {
418+
return nil, fmt.Errorf("failed to cast token to Azure token: %T", accessToken)
419+
}
420+
return &auth.GitCredentials{
421+
BearerToken: token.Token,
422+
ExpiresAt: token.ExpiresOn,
423+
}, nil
424+
}
425+
397426
func (p Provider) impl() Implementation {
398427
if p.Implementation == nil {
399428
return implementation{}

0 commit comments

Comments
 (0)