Skip to content
New: SPIFFE and GitHub Actions identity tokens

tokenex

A Go library that exchanges the identity token your workload already has for short-lived credentials, and keeps them fresh. No keys to store or rotate.

A long-lived key works for anyone who has a copy. It sits in a config file, an environment variable or a CI secret, and someone has to rotate it.

Cloud providers, secret stores and now Anthropic and OpenAI offer workload identity federation instead. The workload proves who it is with a signed token and gets short-lived credentials back. Most workloads already have such a token: SPIRE issues JWT-SVIDs, GitHub Actions issues OIDC tokens, and Kubernetes signs one for every service account. tokenex does the exchange, refreshes the credentials before they expire, and hands each new set to your code on a Go channel.

This is the quick start below: a workload exchanges its SPIRE JWT-SVID for AWS credentials.

SPIRE YOUR GO PROCESS AWS OIDC discovery publishes the keys SPIRE agent Workload API IdentityTokenProvider token/spiffe CredentialsProvider pkg/aws your code for res := range creds IAM OIDC provider trusts SPIRE's issuer AWS STS AssumeRoleWithWebIdentity AWS APIs S3 · DynamoDB · … the issuer's keys (JWKS) 1 JWT-SVID 2 GetToken before they expire, again 3 exchange 4 checks 5 Update <-chan Result 6 SigV4

tokenex’s parts have amber outlines. Swap either one for another identity token source or credentials provider, and the rest stays the same.

  1. The SPIRE agent issues the workload a JWT-SVID through the Workload API. The identity token provider asks for one each time it’s called, so it never hands out a stale token.

  2. The AWS credentials provider gets a token from it, with the audience sts.amazonaws.com.

  3. It exchanges the token at AWS STS with AssumeRoleWithWebIdentity, for the role you named.

  4. STS checks the token’s signature against the keys SPIRE’s OIDC Discovery Provider publishes, which an IAM OIDC identity provider trusts.

  5. The temporary credentials arrive on the channel as an Update. Before they expire, tokenex goes back to step 2 and sends the new ones on the same channel.

  6. Your code signs its AWS calls with them.

Terminal window
go get go.riptides.io/tokenex

Exchange the workload’s SPIRE JWT-SVID for AWS credentials and keep them fresh:

package main
import (
"context"
"log"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/go-logr/logr"
"go.riptides.io/tokenex/pkg/aws"
"go.riptides.io/tokenex/pkg/credential"
"go.riptides.io/tokenex/pkg/token/spiffe"
)
func main() {
ctx := context.Background()
// JWT-SVIDs from the SPIRE agent at SPIFFE_ENDPOINT_SOCKET.
svids, err := spiffe.NewIdentityTokenProvider(spiffe.WithAudience("sts.amazonaws.com"))
if err != nil {
log.Fatal(err)
}
provider, err := aws.NewCredentialsProvider(ctx, logr.Discard(), &awssdk.Config{Region: "us-east-1"})
if err != nil {
log.Fatal(err)
}
creds, err := provider.GetCredentials(ctx, svids,
aws.WithRoleArn("arn:aws:iam::123456789012:role/spire-demo"),
aws.WithRoleSessionName("spire-demo"),
)
if err != nil {
log.Fatal(err)
}
// A fresh result arrives before the previous credentials expire.
for res := range creds {
if res.Err != nil {
log.Fatal(res.Err)
}
c := res.Credential.(*credential.AWSCreds)
log.Printf("access key %s, expires %s", c.AccessKeyID, c.Expires)
}
}

AWS needs an IAM OIDC identity provider that trusts SPIRE’s OIDC Discovery Provider; examples/spire-aws has the setup steps and a runnable version. In CI, swap spiffe for githubactions. For another target, swap aws for any of the providers.

No keys to store

The workload proves who it is with a token it already has. There’s nothing long-lived to put in a config file or a CI secret, and nothing to rotate.

Always fresh

Each provider refreshes its credentials before they expire and sends the new ones on the same channel, with a new identity token for every exchange.

Mix and match

Any identity token source works with any provider that exchanges one: GitHub Actions to Azure, SPIRE to Vault, a static JWT to OpenAI.

AI providers too

Anthropic and OpenAI workload identity federation, alongside AWS, GCP, Azure, OCI, Vault and CyberArk Conjur.

Traced

Credentials providers record OpenTelemetry spans for their exchanges, with the tracer provider you pass or the one in the context.

Open source

MIT. Waterwheel and the Riptides platform use it.

Each provider has its own page with a full example.

  • Static pkg/token

    Holds a JWT you already have; SetToken replaces it.

  • GitHub Actions pkg/token/githubactions

    OIDC ID tokens from the GitHub Actions runtime.

  • SPIFFE pkg/token/spiffe

    JWT-SVIDs from the SPIFFE Workload API, e.g. a SPIRE agent.

To bring your own source, implement token.IdentityTokenProvider or wrap a function with token.IdentityTokenProviderFunc.

Any identity token source above works with each of these.

  • AWS pkg/aws

    Temporary session credentials, through AWS's workload identity federation.

  • GCP pkg/gcp

    Access tokens, through GCP's Workload Identity Federation.

  • Azure pkg/azure

    Access tokens, through Microsoft Entra ID's workload identity federation.

  • OCI pkg/oci

    User principal session tokens (UPST), through OCI's workload identity federation.

  • Vault / OpenBao pkg/vault

    Secrets from Vault or OpenBao, with JWT authentication.

  • CyberArk Conjur pkg/conjur

    Secrets from Conjur's JWT authenticator, polled for rotated values.

  • RFC 7523 pkg/rfc7523

    OAuth2 access tokens with the jwt-bearer grant, including Anthropic workload identity federation.

  • RFC 8693 pkg/rfc8693

    OAuth2 access tokens with token exchange, including OpenAI workload identity federation.

  • Generic pkg/generic

    The identity token itself, refreshed before it expires.

  • Kubernetes Secret pkg/k8ssecret

    Watches a Secret that holds a token, and sends each change.

  • OAuth2 authorization code pkg/oauth2ac

    Access tokens from the authorization code flow, refreshed before they expire.

  • OAuth2 client credentials pkg/oauth2cc

    Access tokens from the client credentials flow, refreshed before they expire.

  • GitHub App pkg/githubapp

    Installation access tokens, minted with the App's private key.

GetCredentials returns a <-chan credential.Result, and every provider uses it the same way:

  • The first credential and each refresh arrive as an Update event.
  • If the credential is removed, a Remove event arrives.
  • On an error, Err is set, Credential is nil and the refresh loop stops. Retrying is up to you.
  • When the loop stops, the channel is closed.

The channel keeps delivering for as long as the context lives, so read it in its own goroutine. To shut down, cancel the context and wait for the goroutine.

var wg sync.WaitGroup
wg.Go(func() {
for res := range creds {
if res.Err != nil {
log.Printf("credentials: %v", res.Err) // the loop has stopped
return
}
switch res.Event {
case credential.UpdateEventType:
// New or refreshed: use res.Credential from now on.
case credential.RemoveEventType:
// The credential is gone: stop using it.
}
}
})
// On shutdown.
cancel()
wg.Wait()

tokenex is built by Riptides.