Skip to content

Commit c4a2b5c

Browse files
authored
Merge pull request #5 from riptideslabs/vaut_gcp_az_access_tokens
Support minting Azure access token from Vault sourced Azure credentials also from Vault sourced GCP service account keys
2 parents 21a089c + db76e37 commit c4a2b5c

13 files changed

Lines changed: 575 additions & 130 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ require (
4242
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
4343
github.com/aws/smithy-go v1.22.4 // indirect
4444
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
45+
github.com/cenkalti/backoff/v5 v5.0.3
4546
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
4647
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
4748
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
@@ -52,7 +53,7 @@ require (
5253
github.com/go-openapi/jsonpointer v0.21.0 // indirect
5354
github.com/go-openapi/jsonreference v0.20.2 // indirect
5455
github.com/go-openapi/swag v0.23.0 // indirect
55-
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
56+
github.com/go-viper/mapstructure/v2 v2.4.0
5657
github.com/gogo/protobuf v1.3.2 // indirect
5758
github.com/google/gnostic-models v0.6.9 // indirect
5859
github.com/google/go-cmp v0.7.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
7878
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
7979
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
8080
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
81+
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
82+
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
8183
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
8284
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8385
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k=

pkg/aws/creds.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,7 @@ func (cp *credentialsProvider) refreshCredentialsLoop(ctx context.Context, provi
7878
// Get credentials
7979
awsCreds, err := provider.Retrieve(ctx)
8080
if err != nil {
81-
util.SendToChannel(credsChan, credential.Result{
82-
Credential: nil,
83-
Err: errors.WrapIf(err, "failed to retrieve credentials"),
84-
})
81+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to retrieve credentials"))
8582

8683
return
8784
}
@@ -111,10 +108,7 @@ func (cp *credentialsProvider) refreshCredentialsLoop(ctx context.Context, provi
111108

112109
// If credentials are already expired, this is an error
113110
if timeUntilExpiry <= 0 {
114-
util.SendToChannel(credsChan, credential.Result{
115-
Credential: nil,
116-
Err: errors.NewWithDetails("received already expired credentials", "expiresAt", awsCreds.Expires),
117-
})
111+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired credentials", "expiresAt", awsCreds.Expires))
118112

119113
return
120114
}

pkg/azure/creds.go

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,17 @@ func (cp *credentialsProvider) refreshCredentialsLoop(
8686
Scopes: []string{cfg.scope},
8787
})
8888
if err != nil {
89-
util.SendToChannel(credsChan, credential.Result{
90-
Credential: nil,
91-
Err: errors.WrapIf(err, "failed to retrieve credentials"),
92-
})
89+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to retrieve credentials"))
90+
91+
return
92+
}
93+
94+
// Calculate when to refresh
95+
timeUntilExpiry := time.Until(token.ExpiresOn)
96+
97+
// If credentials are already expired, this is an error
98+
if timeUntilExpiry <= 0 {
99+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired credentials", "expiresAt", token.ExpiresOn))
93100

94101
return
95102
}
@@ -107,19 +114,6 @@ func (cp *credentialsProvider) refreshCredentialsLoop(
107114
})
108115
cp.logger.V(2).Info("Sent credentials", "expires", token.ExpiresOn)
109116

110-
// Calculate when to refresh
111-
timeUntilExpiry := time.Until(token.ExpiresOn)
112-
113-
// If credentials are already expired, this is an error
114-
if timeUntilExpiry <= 0 {
115-
util.SendToChannel(credsChan, credential.Result{
116-
Credential: nil,
117-
Err: errors.NewWithDetails("received already expired credentials", "expiresAt", token.ExpiresOn),
118-
})
119-
120-
return
121-
}
122-
123117
refreshBuffer := util.CalculateRefreshBuffer(timeUntilExpiry)
124118
refreshTime := timeUntilExpiry - refreshBuffer
125119

pkg/gcp/creds.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ func (cp *credentialsProvider) refreshCredentialsLoop(
9090
for {
9191
accessToken, err := genAccessTokenFunc()
9292
if err != nil {
93-
util.SendToChannel(credsChan, credential.Result{
94-
Credential: nil,
95-
Err: errors.WrapIf(err, "failed to get access token"),
96-
})
93+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to get access token"))
9794

9895
return
9996
}
@@ -113,10 +110,7 @@ func (cp *credentialsProvider) refreshCredentialsLoop(
113110

114111
// If credentials are already expired, this is an error
115112
if timeUntilExpiry <= 0 {
116-
util.SendToChannel(credsChan, credential.Result{
117-
Credential: nil,
118-
Err: errors.NewWithDetails("received already expired credentials", "expiresAt", accessToken.Expiry),
119-
})
113+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired credentials", "expiresAt", accessToken.Expiry))
120114

121115
return
122116
}

pkg/generic/creds.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,7 @@ loop:
159159

160160
token, err = tokenProvider.GetToken(ctx, opts...)
161161
if err != nil {
162-
util.SendToChannel(credsChan, credential.Result{
163-
Credential: nil,
164-
Err: errors.WrapIf(err, "could not create token"),
165-
})
162+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "could not create token"))
166163

167164
return
168165
}
@@ -190,10 +187,7 @@ loop:
190187

191188
// If credentials are already expired, this is an error
192189
if timeUntilExpiry <= 0 {
193-
util.SendToChannel(credsChan, credential.Result{
194-
Credential: nil,
195-
Err: errors.NewWithDetails("received already expired credentials", "expiresAt", token.ExpiresAt.Format(time.DateTime)),
196-
})
190+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired credentials", "expiresAt", token.ExpiresAt.Format(time.DateTime)))
197191

198192
return
199193
}

pkg/oauth2cc/oauth2cc.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,7 @@ func (r *tokenRetriever) tokenRefresherLoop(ctx context.Context) {
262262

263263
// If credentials are already expired, this is an error
264264
if timeUntilExpiry <= 0 {
265-
util.SendToChannel(r.ch, credential.Result{
266-
Credential: nil,
267-
Err: errors.NewWithDetails("received already expired token", "expiresAt", token.Expiry),
268-
})
265+
util.SendErrorToChannel(r.ch, errors.NewWithDetails("received already expired token", "expiresAt", token.Expiry))
269266

270267
return
271268
}

pkg/oci/creds.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,30 +95,21 @@ func (cp *credentialsProvider) refreshCredentialsLoop(ctx context.Context, cfg *
9595
for {
9696
idToken, err := cfg.identityTokenProvider.GetToken(ctx)
9797
if err != nil {
98-
util.SendToChannel(credsChan, credential.Result{
99-
Credential: nil,
100-
Err: errors.WrapIf(err, "failed to get identity token"),
101-
})
98+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to get identity token"))
10299

103100
return
104101
}
105102

106103
authToken, err := exchangeToken(ctx, tokenEndpoint, cfg.clientID, cfg.clientSecret, idToken.Token, publicKey)
107104
if err != nil {
108-
util.SendToChannel(credsChan, credential.Result{
109-
Credential: nil,
110-
Err: errors.WrapIf(err, "token exchange failed"),
111-
})
105+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "token exchange failed"))
112106

113107
return
114108
}
115109

116110
expTime, err := getTokenExpiration(authToken)
117111
if err != nil {
118-
util.SendToChannel(credsChan, credential.Result{
119-
Credential: nil,
120-
Err: errors.WrapIf(err, "failed to get expiration time of the received UPST"),
121-
})
112+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to get expiration time of the received UPST"))
122113

123114
return
124115
}
@@ -134,10 +125,7 @@ func (cp *credentialsProvider) refreshCredentialsLoop(ctx context.Context, cfg *
134125

135126
// If credentials are already expired, this is an error
136127
if timeUntilExpiry <= 0 {
137-
util.SendToChannel(credsChan, credential.Result{
138-
Credential: nil,
139-
Err: errors.NewWithDetails("received already expired credentials", "expiresAt", ociCredential.ExpiresAt),
140-
})
128+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired credentials", "expiresAt", ociCredential.ExpiresAt))
141129

142130
return
143131
}

pkg/util/credential.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,11 @@ func SendToChannel(credsChan chan credential.Result, result credential.Result) {
4444
credsChan <- result
4545
}
4646
}
47+
48+
// SendErrorToChannel is a helper function to send an error result to the credentials channel.
49+
func SendErrorToChannel(credsChan chan credential.Result, err error) {
50+
SendToChannel(credsChan, credential.Result{
51+
Credential: nil,
52+
Err: err,
53+
})
54+
}

pkg/vault/azure.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright (c) 2026 Riptides Labs, Inc.
2+
// SPDX-License-Identifier: MIT
3+
4+
package vault
5+
6+
import (
7+
"context"
8+
"time"
9+
10+
"emperror.dev/errors"
11+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
13+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
14+
"github.com/cenkalti/backoff/v5"
15+
"github.com/go-logr/logr"
16+
17+
"go.riptides.io/tokenex/pkg/credential"
18+
"go.riptides.io/tokenex/pkg/util"
19+
)
20+
21+
// VaultAzureSecret represents the structure of the secret data returned by Vault's Azure secrets engine when configured to return Azure credentials.
22+
type vaultAzureSecret struct {
23+
ClientID string `mapstructure:"client_id"`
24+
ClientSecret string `mapstructure:"client_secret"`
25+
}
26+
27+
type azureAccessTokenProvider struct {
28+
tenantID string
29+
clientID string
30+
clientSecret string
31+
scopes []string
32+
33+
logger logr.Logger
34+
}
35+
36+
// GetCredentials begins the process of exchanging the client ID and client secret for an Azure access token and refreshing it as needed until the context is canceled.
37+
func (r *azureAccessTokenProvider) GetCredentials(ctx context.Context, credsChan chan credential.Result) {
38+
b := backoff.NewExponentialBackOff()
39+
40+
for {
41+
azClientCreds, err := azidentity.NewClientSecretCredential(r.tenantID, r.clientID, r.clientSecret, nil)
42+
if err != nil {
43+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to create Azure client secret credential"))
44+
45+
return
46+
}
47+
48+
token, err := backoff.Retry(ctx, func() (azcore.AccessToken, error) {
49+
token, err := azClientCreds.GetToken(ctx, policy.TokenRequestOptions{
50+
Scopes: r.scopes,
51+
})
52+
53+
return token, err
54+
}, backoff.WithBackOff(b), backoff.WithMaxElapsedTime(30*time.Second))
55+
if err != nil {
56+
util.SendErrorToChannel(credsChan, errors.WrapIf(err, "failed to get Azure access token from client secret credential"))
57+
58+
return
59+
}
60+
61+
// Calculate when to refresh
62+
timeUntilExpiry := time.Until(token.ExpiresOn)
63+
64+
// If credentials are already expired, this is an error
65+
if timeUntilExpiry <= 0 {
66+
util.SendErrorToChannel(credsChan, errors.NewWithDetails("received already expired access token from Azure", "expiresAt", token.ExpiresOn))
67+
68+
return
69+
}
70+
71+
// Send credentials
72+
azureCredential := &credential.Oauth2Creds{
73+
AccessToken: token.Token,
74+
TokenType: "Bearer",
75+
Expiry: token.ExpiresOn,
76+
}
77+
util.SendToChannel(credsChan, credential.Result{
78+
Credential: azureCredential,
79+
Err: nil,
80+
Event: credential.UpdateEventType,
81+
})
82+
r.logger.V(2).Info("Sent credentials", "expires", token.ExpiresOn)
83+
84+
refreshBuffer := util.CalculateRefreshBuffer(timeUntilExpiry)
85+
refreshTime := timeUntilExpiry - refreshBuffer
86+
87+
if !token.RefreshOn.IsZero() {
88+
// if refresh time is recommended in the received token, use that
89+
r.logger.V(2).Info("Using RefreshOn time from token", "refreshOn", token.RefreshOn)
90+
91+
rt := time.Until(token.RefreshOn)
92+
if rt > 0 {
93+
refreshTime = rt
94+
} else {
95+
r.logger.V(2).Info("RefreshOn time is in the past, using calculated refresh time", "refreshTime", refreshTime)
96+
}
97+
}
98+
99+
select {
100+
case <-ctx.Done():
101+
r.logger.V(1).Info("Context cancelled, stopping credential refresh")
102+
103+
return
104+
case <-time.After(refreshTime):
105+
// Continue to next iteration to refresh
106+
r.logger.V(2).Info("Refreshing access token")
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)