Introducing tokenex: an open source Go library for fetching and refreshing cloud credentials
Why we built tokenex
In modern cloud systems, long-lived secrets are a liability. They sprawl across environments, get baked into config files, and eventually leak. The industry’s response has been short-lived, federated credentials as AWS session tokens, GCP and Azure access tokens, OCI UPSTs—that reduce exposure and eliminate the need to persist secrets inside workloads.
Even with provider SDKs offering automatic refresh, workloads spanning multiple clouds face a hidden challenge: each provider enforces its own configuration and credential exchange flow. Managing these in isolation quickly becomes cumbersome. This is especially true for non-human identities, where workloads must authenticate seamlessly across providers. The goal is clear, secretless, automated access: workloads should acquire short-lived credentials on demand, refresh them transparently, and never rely on long-lived secrets.
As we’ve explored in earlier posts (Why cloud-native federation isn’t enough, and Workload identity without secrets), the challenge isn’t just about obtaining credentials once, it’s about securely acquiring, refreshing, and distributing them automatically across providers in a world that’s rapidly shifting to the post-credential era.”
Our broader architecture needed a common building block that could:
- Take an identity token from an Identity Provider,
- Exchange it for the appropriate credential in each cloud/provider, and
- Continuously refresh that credential so workloads always have a valid one on hand.
No such abstraction existed in the ecosystem. So we created tokenex: a Go library that abstracts away the messy details of token exchange and refresh behind a single, consistent API. With tokenex, services can stay focused on their core functionality, without ever touching long-lived secrets or managing credential lifecycles themselves.
What the tokenex library does
- Purpose: tokenex is a modular Go library for fetching and refreshing cloud credentials and tokens. It abstracts credential acquisition, refresh, and configuration for AWS, GCP, Azure, OCI, OAuth2, and generic tokens from identity token providers.
Features
- AWS: Exchanges ID tokens for AWS temporary session credentials using AWS Workload Identity Federation.
- GCP: Exchanges ID tokens for GCP access tokens using GCP Workload Identity Federation.
- Azure: Exchanges ID tokens for Azure access tokens using Microsoft Entra ID Workload Identity Federation.
- OCI: Exchanges ID tokens for OCI User Principal Session Tokens (UPST) using OCI Workload Identity Federation.
- Generic: Returns the token provided by the identity token provider and refreshes it before expiration.
- K8sSecret: Watches a Kubernetes secret that contains a token and publishes updates when the secret changes.
- OAuth2AC: Obtains access tokens through the OAuth2 authorization code flow and refreshes them before expiration.
- OAuth2CC: Obtains access tokens through the OAuth2 client credentials flow and refreshes them before expiration.
Benefits
- Consistency: Offers a consistent API for credential management, regardless of provider.
- Extensibility: New providers or token types can be added with minimal friction by following the established provider pattern.
- Configurability: Uses the Go "option" pattern, allowing users to configure providers with composable options (e.g.,
WithClientID
, WithScope
, etc.). - Asynchronous & reactive: Credentials are delivered via channels, supporting reactive and non-blocking workflows.
How it works
All credential providers in this library follow a consistent pattern for credential delivery:
- The
GetCredentials
method returns a channel that receives credential updates. - For the first credential and each refresh, an
Update
event is sent. - If credentials are removed, a
Remove
event is sent. - In case of errors, the
Err
field is populated, Credential
is nil, and the refresh loop exits. - When the refresh loop exits, the channel is closed.
This design ensures that credentials are always up‑to‑date and that applications can handle refreshes or errors reactively.
Graceful Shutdown
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.
Configurability
- Option pattern: Each provider exposes a set of WithX functions (e.g.,
WithClientID
, WithIdentityTokenProvider
) to configure credentials at construction time. - Provider construction: Providers are created with
NewCredentialsProvider(...)
, accepting context, logger, and provider-specific configs. - Custom token providers: The token package allows injecting custom
IdentityTokenProvider
implementations.
Extensibility
- Adding providers: To add a new provider, implement a new subpackage with a struct that implements the
CredentialsProvider
interface. - Custom options: New options can be added by extending the option pattern in each provider.
Summary table
Feature |
Description
|
Providers
|
AWS, GCP, Azure, OCI, OAuth2 (AC/CC), Generic
|
Config pattern
|
Go option pattern (WithX functions)
|
Async support
|
Credentials delivered via channels |
Extensible
|
Via new subpackages and option pattern |
Use cases
|
Multi-cloud credential management, token exchange, secure service auth |
Getting started
This is a simple application that demonstrates how to use tokenex to obtain temporary credentials, called a User Principal Session Token (UPST), from OCI. The UPST is then used to authenticate and list users in a tenancy via the OCI Go SDK.
This blog post uses OCI as an example. If you’re looking for AWS, GCP, Azure, Kubernetes, or generic secret provider integrations, check out the tokenex repository on GitHub.
The application consists of two goroutines:
- UPST retrieval: Responsible for exchanging an ID token (issued by an external IDP) for a UPST using tokenex.
- Workload simulation: A simple workload that uses the UPST to authenticate with OCI and list tenancy users.
How UPST retrieval works
The first goroutine leverages tokenex to retrieve UPSTs from OCI in exchange for an ID token from an external IDP. Under the hood, tokenex uses OCI's Workload Identity Federation to handle the token exchange.
To enable this, you must configure trust between your IDP and OCI. For setup instructions, see the OCI Workload Identity Federation guide, specifically the section on Identity Propagation Trust Configuration. Without this setup, OCI will reject the ID token when tokenexattempts the exchange.
This sample assumes:
- OCI trusts the external IDP.
- The trust configuration maps the ID token to a service user with privileges to list tenancy users.
Important note on OCI Go SDK Support
Currently, the OCI Go SDK does not natively support consuming UPSTs directly for authentication. To work around this, we create an OCI SDK config file that uses the received UPST for authentication.
Riptides' approach avoids persisting sensitive credentials (UPST and private keys) to disk. Instead, the UPST is injected on the wire during the ListUsers
API call. This significantly strengthens security by eliminating the need for local credential storage.
For a deeper dive into how this works, check out these related posts:
Sample application
package main
import (
"context"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"log/slog"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-logr/logr"
"go.riptides.io/tokenex/pkg/credential"
"go.riptides.io/tokenex/pkg/oci"
"go.riptides.io/tokenex/pkg/token"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identity"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop() // clean up signal handler
logger := logr.FromSlogHandler(slog.Default().Handler())
logger.Info("Press Ctrl+C to stop...")
// setup credential provider to receive OCI credentials
// create OCI credentials provider
credProvider, err := oci.NewCredentialsProvider(ctx, logger)
if err != nil {
logger.Error(err, "failed to create OCI credentials provider")
return
}
// under the hood the credential provider uses OCI workload identity federation to fetch user principal session tokens from OCI
// the credential provider exchanges an input ID token for an OCI user principal session token
// the input ID token can be obtained from any OIDC compliant IDP (e.g. Google, Microsoft, Auth0, Okta, etc.)
// for this example, we use a static ID token provider that returns a hardcoded ID token issued by an OIDC compliant IDP
// in a real application, you would implement the `token.IdentityTokenProvider` interface to create a dynamic ID token provider that fetches the ID token from an OIDC compliant IDP
idTokenProvider := token.NewStaticIdentityTokenProvider("<id-token-issued-by-idp>")
// create RSA key pair for the application(workload) that is going to use the OCI user principal session tokens for authentication in order to be able to invoke OCI services
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
logger.Error(err, "failed to generate RSA key pair")
return
}
privateKeyDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
logger.Error(err, "failed to marshal RSA private key to DER format")
return
}
publicKeyDer, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
logger.Error(err, "failed to marshal RSA public key to DER format")
return
}
hash := md5.Sum(publicKeyDer)
parts := make([]string, len(hash))
for i, b := range hash {
parts[i] = fmt.Sprintf("%02X", b)
}
fingerPrint := strings.Join(parts, ":")
// currently the OCI SDK for Go v2 does not support using user principal session tokens for authentication directly
// thus we either create a custom `common.ConfigurationProvider` that uses the user principal session tokens for authentication
// or we use the `common.ConfigurationProviderForSessionToken` helper function
// we use the later for this example and for this we need to create an OCI config file which will use the user principal session tokens we receive from the credential provider
privateKeyFile, err := os.CreateTemp("", "private_key_*.pem")
if err != nil {
logger.Error(err, "failed to create private key file")
return
}
defer os.Remove(privateKeyFile.Name())
privateKeyBlock := &pem.Block{
Type: "PRIVATE KEY",
Bytes: privateKeyDer,
}
privateKeyPem := pem.EncodeToMemory(privateKeyBlock)
if err := os.WriteFile(privateKeyFile.Name(), privateKeyPem, 0400); err != nil {
logger.Error(err, "failed to write private key file")
return
}
sessionTokenFile, err := os.CreateTemp("", "session_token_*")
if err != nil {
logger.Error(err, "failed to create session token file")
return
}
defer os.Remove(sessionTokenFile.Name())
// create OCI config file that uses the session token file for authentication
ociConfigFile, err := os.CreateTemp("", "oci_config_*")
if err != nil {
logger.Error(err, "failed to create OCI config file")
return
}
defer os.Remove(ociConfigFile.Name())
// write OCI config file
ociConfig := strings.Join([]string{
"[DEFAULT]",
fmt.Sprintf("region=%s", "eu-frankfurt-1"),
fmt.Sprintf("fingerprint=%s", fingerPrint),
fmt.Sprintf("tenancy=%s", "<tenancy-id>"),
fmt.Sprintf("key_file=%s", privateKeyFile.Name()),
fmt.Sprintf("security_token_file=%s", sessionTokenFile.Name()),
}, "\n")
if err := os.WriteFile(ociConfigFile.Name(), []byte(ociConfig), 0400); err != nil {
logger.Error(err, "failed to write OCI config file")
return
}
// get OCI credentials
creds, err := credProvider.GetCredentials(ctx,
idTokenProvider, // supplies the ID token issued by an OIDC compliant IDP for the application(workload) that is going to use the OCI user principal session tokens for authentication.
oci.WithClientID("<client-id>"), // client ID of the application registered in OCI which is allowed to exchange ID tokens for OCI user principal session tokens.
oci.WithClientSecret("<client-secret>"), // client secret of the application registered in OCI which is allowed to exchange ID tokens for OCI user principal session tokens.
oci.WithIdentityDomainURL("<identity-domain-url>"), // identity domain URL of the OCI tenancy where the application which is allowed to exchange ID tokens is registered.
oci.WithRsaPublicKeyDer([]byte("<rsa-public-key-der>")), // RSA public key in DER format of the application(workload) which is going to use the OCI user principal session tokens for authentication.
)
if err != nil {
logger.Error(err, "failed to get OCI credentials")
return
}
// retrieve OCI credentials and updates before they expire for the identity that corresponds to the provided ID token
go func() {
defer stop()
for {
select {
case <-ctx.Done():
return
case credentialEvent := <-creds:
if credentialEvent.Err != nil {
logger.Error(credentialEvent.Err, "failed to get OCI credentials")
return
}
token, ok := credentialEvent.Credential.(*credential.Token)
if !ok {
logger.Error(err, "failed to assert credential type")
return
}
logger.Info("received new OCI credentials", "user principal session token", token.Token, "expires at", token.ExpiresAt.String())
// write session token to file; if the file already exists update it's content with a fresh session token
if err := os.WriteFile(sessionTokenFile.Name(), []byte(token.Token), 0400); err != nil {
logger.Error(err, "failed to write session token file")
return
}
}
}
}()
time.Sleep(5 * time.Second) // wait for initial credentials
// use OCI credentials to call OCI services
// in this example we use the OCI SDK for Go v2 to list the users in the OCI tenancy
// the OCI SDK for Go v2 will use the OCI config file we created above which uses the user principal session tokens for authentication
go func() {
defer stop()
// create OCI identity client
configProvider, err := common.ConfigurationProviderForSessionToken(ociConfigFile.Name(), "")
if err != nil {
logger.Error(err, "failed to create OCI configuration provider")
return
}
identityClient, err := identity.NewIdentityClientWithConfigurationProvider(configProvider)
if err != nil {
logger.Error(err, "failed to create OCI identity client")
return
}
tenancyID, _ := configProvider.TenancyOCID()
req := identity.ListUsersRequest{
CompartmentId: &tenancyID, // The OCID of the compartment (remember that the tenancy is simply the root compartment).
}
logger.Info("simulating application work...")
listUsers := func() error {
resp, err := identityClient.ListUsers(ctx, req)
if err != nil {
return err
}
for _, user := range resp.Items {
logger.Info("user", "username", *user.Name)
}
logger.Info("----")
return nil
}
// simulate application doing some work
listUsers()
for {
select {
case <-ctx.Done():
return
case <-time.After(10 * time.Minute):
err = listUsers()
if err != nil {
logger.Error(err, "failed to list OCI users")
return
}
}
}
}()
<-ctx.Done()
logger.Info("context cancelled, exiting")
}
Final thoughts
With tokenex, you no longer need to juggle cloud-specific SDKs or write custom refresh logic. It provides a unified, extensible, and reactive way to handle credentials across providers—out of the box.
We’re excited to open source this library and invite the community to try it, give feedback, and contribute.
👉 Check out the code and documentation on GitHub: riptideslabs/tokenex
If you enjoyed this post, follow us on LinkedIn and X for more updates. If you’d like to see Riptides in action, get in touch with us for a demo.