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:
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.
WithClientID, WithScope, etc.).All credential providers in this library follow a consistent pattern for credential delivery:
GetCredentials method returns a channel that receives credential updates.Update event is sent.Remove event is sent.Err field is populated, Credential is nil, and the refresh loop exits.This design ensures that credentials are always up‑to‑date and that applications can handle refreshes or errors reactively.
For proper application shutdown, always:
This ensures that all resources are properly cleaned up and prevents goroutine leaks.
WithClientID, WithIdentityTokenProvider) to configure credentials at construction time.NewCredentialsProvider(...), accepting context, logger, and provider-specific configs.IdentityTokenProvider implementations.CredentialsProvider interface.| 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 |
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:
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 tokenex attempts the exchange.
This sample assumes:
Currently, the OCI Go SDK does not natively support consuming UPSTs directly for authentication. To work around this, 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:
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")
}
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