This library provides a unified interface for obtaining and refreshing credentials from various cloud providers and authentication systems. It is designed to facilitate secure access to cloud resources by exchanging identity tokens for temporary credentials.
- Introducing tokenex: an open source Go library for fetching and refreshing credentials
- tokenex adds Vault & OpenBao support: Exchanging ID tokens (JWTs) for secrets without static credentials
- Supplying short-lived OpenAI API keys to AI agents with Riptides
- Secretless Azure access with tokenex: Federated Identity via User-Assigned Managed Identity
- AWS: Exchanges ID tokens for AWS temporary session credentials using AWS's Workload Identity Federation
- GCP: Exchanges ID tokens for GCP access tokens using GCP's Workload Identity Federation
- Azure: Exchanges ID tokens for Azure access tokens using Microsoft Entra ID's Workload Identity Federation
- OCI: Exchanges ID tokens for OCI User Principal Session Tokens (UPST) using OCI's Workload Identity Federation
- Generic: Simply returns the token provided by the identity token provider and refreshes it before expiration
- K8sSecret: Watches a Kubernetes secret which contains a token and publishes updates when the secret changes
- OAuth2AC: Obtains access tokens through OAuth2 authorization code flow and refreshes them before expiration
- OAuth2CC: Obtains access tokens through OAuth2 client credentials flow and refreshes them before expiration
- Vault Exchanges ID tokens for secrets from Vault using Vault's JWT authentication.
- GitHub App: Mints GitHub App installation access tokens by signing a JWT with the App's private key and refreshes them before expiration.
To use the credentials providers, ensure you have Go installed and set up your project to include the necessary dependencies.
go get go.riptides.io/tokenexBelow are examples demonstrating how to use each credential provider in the library.
import (
"context""log""os""os/signal""sync""syscall""time""go.riptides.io/tokenex/pkg/token"
)
// Create a cancellable contextctx, cancel:=context.WithCancel(context.Background())
defercancel()
// Set up graceful shutdownsignalChan:=make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
gofunc() {
sig:=<-signalChanlog.Printf("Received signal: %v, shutting down...", sig)
cancel()
}()
// Create a wait group to track goroutinesvarwg sync.WaitGroup// Create a loggerlogger:=log.Default()
// Create an identity token provider// This is used by most credential providers to exchange for service-specific tokensidToken:="your-id-token"idTokenProvider:=token.NewStaticIdentityTokenProvider(idToken)import (
"go.riptides.io/tokenex/pkg/aws""go.riptides.io/tokenex/pkg/credential"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
)
// Create the AWS credentials providerawsProvider, err:=aws.NewCredentialsProvider(ctx, logger, &awssdk.Config{Region: "us-west-2"})
iferr!=nil {
log.Fatalf("Failed to create AWS credentials provider: %v", err)
}
// Get AWS credentialsawsCredsChan, err:=awsProvider.GetCredentials(
ctx,
idTokenProvider,
aws.WithRoleArn("arn:aws:iam::123456789012:role/example-role"),
aws.WithRoleSessionName("example-session"),
)
iferr!=nil {
log.Fatalf("Failed to get AWS credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-awsCredsChan:
if!ok {
log.Println("AWS credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
awsCreds:=creds.Credential.(*credential.AWSCreds)
log.Printf("Access Key ID: %s", awsCreds.AccessKeyID)
// Use the credentials...case<-ctx.Done():
log.Println("Context cancelled, shutting down AWS credentials handler")
return
}
}
}()
// In a real application, you would do other work here// ...// Wait for graceful shutdown when your application is terminating// wg.Wait() // Uncomment this in your actual applicationimport (
"go.riptides.io/tokenex/pkg/gcp""go.riptides.io/tokenex/pkg/credential"
)
// Create the GCP credentials providergcpProvider, err:=gcp.NewCredentialsProvider(ctx, logger)
iferr!=nil {
log.Fatalf("Failed to create GCP credentials provider: %v", err)
}
// Get GCP credentialsgcpCredsChan, err:=gcpProvider.GetCredentials(
ctx,
idTokenProvider,
gcp.WithAudience("//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/example-pool/providers/example-provider"),
gcp.WithServiceAccountEmail("service-account@project-id.iam.gserviceaccount.com"),
)
iferr!=nil {
log.Fatalf("Failed to get GCP credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-gcpCredsChan:
if!ok {
log.Println("GCP credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
gcpCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Access Token: %s", gcpCreds.AccessToken)
// Use the credentials...case<-ctx.Done():
log.Println("Context cancelled, shutting down GCP credentials handler")
return
}
}
}()import (
"go.riptides.io/tokenex/pkg/azure""go.riptides.io/tokenex/pkg/credential"
)
// Create the Azure credentials providerazureProvider, err:=azure.NewCredentialsProvider(ctx, logger)
iferr!=nil {
log.Fatalf("Failed to create Azure credentials provider: %v", err)
}
// Get Azure credentialsazureCredsChan, err:=azureProvider.GetCredentials(
ctx,
idTokenProvider,
azure.WithClientID("your-client-id"),
azure.WithTenantID("your-tenant-id"),
azure.WithScope("https://management.azure.com/.default"),
)
iferr!=nil {
log.Fatalf("Failed to get Azure credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-azureCredsChan:
if!ok {
log.Println("Azure credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
azureCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Access Token: %s", azureCreds.AccessToken)
// Use the credentials...case<-ctx.Done():
log.Println("Context cancelled, shutting down Azure credentials handler")
return
}
}
}()import (
"go.riptides.io/tokenex/pkg/oci""go.riptides.io/tokenex/pkg/credential"
)
// Create the OCI credentials providerociProvider, err:=oci.NewCredentialsProvider(ctx, logger)
iferr!=nil {
log.Fatalf("Failed to create OCI credentials provider: %v", err)
}
// Get OCI credentialsociCredsChan, err:=ociProvider.GetCredentials(
ctx,
idTokenProvider,
oci.WithClientID("your-client-id"),
oci.WithIdentityDomainURL("https://idcs-example.identity.oraclecloud.com"),
)
iferr!=nil {
log.Fatalf("Failed to get OCI credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-ociCredsChan:
if!ok {
log.Println("OCI credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
ociCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Access Token: %s", ociCreds.AccessToken)
// Use the credentials...case<-ctx.Done():
log.Println("Context cancelled, shutting down OCI credentials handler")
return
}
}
}()import (
"go.riptides.io/tokenex/pkg/generic""go.riptides.io/tokenex/pkg/credential"
)
// Create the Generic credentials providergenericProvider, err:=generic.NewCredentialsProvider(ctx, logger)
iferr!=nil {
log.Fatalf("Failed to create Generic credentials provider: %v", err)
}
// Get Generic credentials (passes through the identity token)genericCredsChan, err:=genericProvider.GetCredentials(ctx, idTokenProvider)
iferr!=nil {
log.Fatalf("Failed to get Generic credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-genericCredsChan:
if!ok {
log.Println("Generic credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
genericCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Token: %s", genericCreds.AccessToken)
// Use the token...case<-ctx.Done():
log.Println("Context cancelled, shutting down Generic credentials handler")
return
}
}
}()import (
"go.riptides.io/tokenex/pkg/k8ssecret""go.riptides.io/tokenex/pkg/credential""sigs.k8s.io/controller-runtime/pkg/cache"
)
// Assume you have already initialized a Kubernetes client and cache// This typically involves:// 1. Getting a Kubernetes config (config.GetConfig())// 2. Creating a controller-runtime cache (cache.New())// 3. Starting the cache (cache.Start(ctx))k8sCache:=yourInitializedCache// Create the K8sSecret credentials providerk8sProvider, err:=k8ssecret.NewCredentialsProvider(ctx, k8sCache)
iferr!=nil {
log.Fatalf("Failed to create K8sSecret credentials provider: %v", err)
}
// Define the secret referencesecretRef:= k8ssecret.SecretRef{
Name: "token-secret",
Namespace: "default",
Key: "token",
}
// Get credentials from the Kubernetes secretk8sCredsChan, err:=k8sProvider.GetCredentials(ctx, secretRef)
iferr!=nil {
log.Fatalf("Failed to get K8sSecret credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-k8sCredsChan:
if!ok {
log.Println("K8sSecret credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
k8sCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Token: %s", k8sCreds.AccessToken)
// Use the token...case<-ctx.Done():
log.Println("Context cancelled, shutting down K8sSecret credentials handler")
return
}
}
}()import (
"errors""go.riptides.io/tokenex/pkg/oauth2ac""go.riptides.io/tokenex/pkg/credential""github.com/go-logr/logr""golang.org/x/oauth2""sigs.k8s.io/controller-runtime/pkg/cache"
)
// Assume you have already initialized a Kubernetes client and cache// This typically involves:// 1. Getting a Kubernetes config (config.GetConfig())// 2. Creating a controller-runtime cache (cache.New())// 3. Starting the cache (cache.Start(ctx))k8sCache:=yourInitializedCache// Create a loggerlogger:=logr.New(logr.Discard())
// Create a token storage implementation// This is a simple in-memory implementation for example purposestypeinMemoryTokenStoragestruct {
tokensmap[string]*oauth2.Token
}
funcnewInMemoryTokenStorage() *inMemoryTokenStorage {
return&inMemoryTokenStorage{
tokens: make(map[string]*oauth2.Token),
}
}
func (s*inMemoryTokenStorage) Get(ctx context.Context, idstring) (*oauth2.Token, error) {
token, ok:=s.tokens[id]
if!ok {
returnnil, errors.New("token not found")
}
returntoken, nil
}
func (s*inMemoryTokenStorage) Store(ctx context.Context, idstring, token*oauth2.Token) error {
s.tokens[id] =tokenreturnnil
}
func (s*inMemoryTokenStorage) Delete(ctx context.Context, idstring) error {
delete(s.tokens, id)
returnnil
}
tokenStorage:=newInMemoryTokenStorage()
// Define the OAuth2 configurationconfig:=&oauth2ac.CredentialsConfig{
AuthorizationEndpointURL: "https://auth.example.com/oauth2/authorize",
TokenEndpointURL: "https://auth.example.com/oauth2/token",
RedirectURL: "https://localhost:8080/callback",
Scopes: []string{"openid", "profile"},
UsePKCE: true, // Use PKCE for added securitySecretRef: oauth2ac.SecretRef{
Name: "oauth-secret",
Namespace: "default",
Key: "credentials", // Contains <client_id:client_secret>
},
}
// Create the OAuth2AC credentials provideroauth2Provider, err:=oauth2ac.NewCredentialsProvider(
"my-oauth-provider", // Unique ID for this providerk8sCache,
tokenStorage,
config,
logger,
)
iferr!=nil {
log.Fatalf("Failed to create OAuth2AC credentials provider: %v", err)
}
// Start the authorization flowstatusChan, err:=oauth2Provider.Start(ctx)
iferr!=nil {
log.Fatalf("Failed to start OAuth2 flow: %v", err)
}
// Monitor the authorization statusgofunc() {
forstatus:=rangestatusChan {
log.Printf("Auth Status: %v", status.Event)
switchstatus.Event {
caseoauth2ac.UnauthorizesStatusEvent:
// User needs to authorizelog.Printf("Authorization required")
// Generate authorization URL for the user to visitstate, authURL:=oauth2Provider.AuthCodeURL(ctx)
log.Printf("Please visit: %s", authURL)
log.Printf("State: %s (save this to verify the callback)", state)
caseoauth2ac.AuthorizedStatusEvent:
log.Printf("Successfully authorized")
// At this stage, the initial token is available in token storage// and will be automatically refreshed before expirationdefault:
// Handle unexpected event typeifstatus.Err!=nil {
log.Printf("Auth Error: %v", status.Err)
// You might want to retry or exit depending on the error
}
}
}
}()
// In a real application, you would have a callback endpoint (HTTP handler) that receives// the authorization code and state when the user is redirected back from the authorization server.// For example, if your redirect URL is "https://localhost:8080/callback", you would have// an HTTP handler for that path that extracts the code and state from the request://// HTTP handler example (not part of this sample):funccallbackHandler(w http.ResponseWriter, r*http.Request) {
// Extract code and state from the requestcode:=r.URL.Query().Get("code")
state:=r.URL.Query().Get("state")
// Verify the state matches what you generated (to prevent CSRF attacks)// Then complete the authorization flow as shown belowtoken, err:=oauth2Provider.Authorize(ctx, state, code)
iferr!=nil {
http.Error(w, "Authorization failed: "+err.Error(), http.StatusInternalServerError)
return
}
// After successful authorization, the credentials channel will receive the token// and refresh it automatically before it expireslog.Printf("Successfully authorized! Token expires at: %v", token.Expiry)
// Inform the user that authorization was successfulw.Write([]byte("Successfully authorized! You can close this window."))
}
// Process credentials from the channel in a goroutine with proper context handlingoauth2CredsChan, err:=oauth2Provider.GetCredentials(ctx)
iferr!=nil {
log.Fatalf("Failed to get OAuth2AC credentials: %v", err)
}
wg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-oauth2CredsChan:
if!ok {
log.Println("OAuth2AC credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
oauth2Creds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Access Token: %s", oauth2Creds.AccessToken)
// Use the token...case<-ctx.Done():
log.Println("Context cancelled, shutting down OAuth2AC credentials handler")
return
}
}
}()import (
"go.riptides.io/tokenex/pkg/oauth2cc""go.riptides.io/tokenex/pkg/credential""sigs.k8s.io/controller-runtime/pkg/cache"
)
// Assume you have already initialized a Kubernetes client and cache// This typically involves:// 1. Getting a Kubernetes config (config.GetConfig())// 2. Creating a controller-runtime cache (cache.New())// 3. Starting the cache (cache.Start(ctx))k8sCache:=yourInitializedCache// Create the OAuth2CC credentials provideroauth2ccProvider, err:=oauth2cc.NewCredentialsProvider(ctx, k8sCache)
iferr!=nil {
log.Fatalf("Failed to create OAuth2CC credentials provider: %v", err)
}
// Define the secret reference containing client ID and secret// The secret should contain a key with value in format "client_id:client_secret"secretRef:= oauth2cc.SecretRef{
Name: "oauth-secret",
Namespace: "default",
Key: "credentials", // Key containing "client_id:client_secret"
}
// Get OAuth2 credentialsoauth2ccCredsChan, err:=oauth2ccProvider.GetCredentials(
ctx,
"https://auth.example.com/oauth2/token",
secretRef,
oauth2cc.WithScope("api.read api.write"),
)
iferr!=nil {
log.Fatalf("Failed to get OAuth2CC credentials: %v", err)
}
// Process credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-oauth2ccCredsChan:
if!ok {
log.Println("OAuth2CC credentials channel closed")
return
}
ifcreds.Err!=nil {
log.Printf("Error: %v", creds.Err)
return
}
oauth2ccCreds:=creds.Credential.(*credential.Oauth2Creds)
log.Printf("Access Token: %s", oauth2ccCreds.AccessToken)
// Use the token...case<-ctx.Done():
log.Println("Context cancelled, shutting down OAuth2CC credentials handler")
return
}
}
}()
// In a real application, you would wait for all goroutines to complete before exiting// wg.Wait()import (
"go.riptides.io/tokenex/pkg/credential""go.riptides.io/tokenex/pkg/token""go.riptides.io/tokenex/pkg/vault"
)
// Create a loggerlogger:=logr.New(logr.Discard())
// Create the Vault credentials providervaultProvider, err:=vault.NewCredentialsProvider(ctx, logger, "http://localhost:8200", nil)
iferr!=nil {
returnerr
}
// Get credentials from VaultdbCredsChan, err:=vaultProvider.GetCredentials(
ctx,
idTokenProvider,
vault.WithJWTAuthMethodPath("jwt"),
vault.WithJWTAuthRoleName("dbuser"),
vault.WithSecretFullPath("database/creds/pg-dyn-dbuser"),
)
iferr!=nil {
returnerr
}
// Process dynamic credentials from the channel in a goroutine with proper context handlingwg.Add(1)
gofunc() {
deferwg.Done()
for {
select {
casecreds, ok:=<-dbCredsChan:
if!ok {
logger.Info("Database credentials channel closed")
return
}
ifcreds.Err!=nil {
logger.Error(creds.Err, "Error receiving database credentials", errors.GetDetails(creds.Err))
return
}
dbSecret:=creds.Credential.(*credential.VaultSecret)
// Database secrets typically contain username and passwordifusername, ok:=dbSecret.Data["username"].(string); ok {
logger.Info("Database username", "value", username)
}
ifpassword, ok:=dbSecret.Data["password"].(string); ok {
logger.Info("Database password", "value", password)
}
case<-ctx.Done():
log.Println("Context cancelled, shutting down database credentials handler")
return
}
}
}()
// In a real application, you would wait for all goroutines to complete before exiting// wg.Wait()import (
"os""github.com/google/go-github/v66/github""go.riptides.io/tokenex/pkg/credential""go.riptides.io/tokenex/pkg/githubapp"
)
// Source the App's private key however you like (env var shown; could be a file,// k8s secret, Vault, etc.) and parse it once.key, err:=githubapp.ParsePrivateKey([]byte(os.Getenv("GITHUB_APP_PRIVATE_KEY")))
iferr!=nil {
log.Fatalf("parse private key: %v", err)
}
provider, err:=githubapp.NewCredentialsProvider(ctx, logr.Discard())
iferr!=nil {
log.Fatalf("new provider: %v", err)
}
credCh, err:=provider.GetCredentials(ctx,
githubapp.WithAppID(123456),
githubapp.WithInstallationID(7890123),
githubapp.WithPrivateKey(key),
// Optional: scope the token to specific repos / permissions.// githubapp.WithRepositories([]string{"repo-a"}),// githubapp.WithPermissions(&github.InstallationPermissions{Contents: github.String("read")}),// Optional: GitHub Enterprise Server.// githubapp.WithBaseURL("https://github.example.com/api/v3"),
)
iferr!=nil {
log.Fatalf("get credentials: %v", err)
}
forcred:=rangecredCh {
ifcred.Err!=nil {
log.Printf("github app token error: %v", cred.Err)
break
}
tok:=cred.Credential.(*credential.Token)
log.Printf("got installation token, expires %s", tok.ExpiresAt)
}All credential providers in this library follow a consistent pattern for credential delivery:
- The
GetCredentialsmethod returns a channel that receives credential updates - For the first credential and each refresh, an
Updateevent is sent - If credentials are removed, a
Removeevent is sent - In case of errors, the
Errfield is populated,Credentialis nil, and the refresh loop exits - When the refresh loop exits, the channel is closed
Important: Since these channels continuously provide credential updates (including automatic refreshes), they should be processed in a goroutine to avoid blocking the main execution flow, as shown in the examples above.
When processing credentials, you can check the event type to determine what action to take:
switchresult.Event {
casecredential.UpdateEventType:
// Use the updated credentialslog.Printf("Received new/refreshed credentials")
// Use result.Credential for API callscasecredential.RemoveEventType:
// Handle credential removallog.Printf("Credentials were removed")
}For proper application shutdown, always:
- Cancel the context when your application is terminating
- Wait for all credential handling goroutines to complete using a wait group
- Handle channel closure and context cancellation in your credential processing loops
This ensures that all resources are properly cleaned up and prevents goroutine leaks.
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.