Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
984 changes: 74 additions & 910 deletions README.md

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions docs/aws.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# AWS Credentials Provider

Exchanges ID tokens for AWS temporary session credentials using AWS's Workload Identity Federation.

Package [`pkg/aws`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/aws). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```go
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 provider
awsProvider, err := aws.NewCredentialsProvider(ctx, logger, &awssdk.Config{Region: "us-west-2"})
if err != nil {
log.Fatalf("Failed to create AWS credentials provider: %v", err)
}

// Get AWS credentials
awsCredsChan, err := awsProvider.GetCredentials(
ctx,
idTokenProvider,
aws.WithRoleArn("arn:aws:iam::123456789012:role/example-role"),
aws.WithRoleSessionName("example-session"),
)
if err != nil {
log.Fatalf("Failed to get AWS credentials: %v", err)
}

// Process credentials from the channel in a goroutine with proper context handling
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case creds, ok := <-awsCredsChan:
if !ok {
log.Println("AWS credentials channel closed")
return
}
if creds.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 application
```
57 changes: 57 additions & 0 deletions docs/azure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Azure Credentials Provider

Exchanges ID tokens for Azure access tokens using Microsoft Entra ID's Workload Identity Federation.

Package [`pkg/azure`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/azure). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```go
import (
"go.riptides.io/tokenex/pkg/azure"
"go.riptides.io/tokenex/pkg/credential"
)

// Create the Azure credentials provider
azureProvider, err := azure.NewCredentialsProvider(ctx, logger)
if err != nil {
log.Fatalf("Failed to create Azure credentials provider: %v", err)
}

// Get Azure credentials
azureCredsChan, err := azureProvider.GetCredentials(
ctx,
idTokenProvider,
azure.WithClientID("your-client-id"),
azure.WithTenantID("your-tenant-id"),
azure.WithScope("https://management.azure.com/.default"),
)
if err != nil {
log.Fatalf("Failed to get Azure credentials: %v", err)
}

// Process credentials from the channel in a goroutine with proper context handling
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case creds, ok := <-azureCredsChan:
if !ok {
log.Println("Azure credentials channel closed")
return
}
if creds.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
}
}
}()
```
44 changes: 44 additions & 0 deletions docs/common-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Common Setup

The provider docs build on this setup: a cancellable `ctx`, a `logger`, a `wg` for the goroutines that read credential channels, and an `idTokenProvider`.

```go
import (
"context"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"go.riptides.io/tokenex/pkg/token"
)

// Create a cancellable context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Set up graceful shutdown
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalChan
log.Printf("Received signal: %v, shutting down...", sig)
cancel()
}()

// Create a wait group to track goroutines
var wg sync.WaitGroup

// Create a logger
logger := log.Default()

// Create an identity token provider
// This is used by most credential providers to exchange for service-specific tokens
idToken := "your-id-token"
idTokenProvider := token.NewStaticIdentityTokenProvider(idToken)
```

## Identity token providers

Providers that exchange an identity token take a `token.IdentityTokenProvider`. The static provider above holds a JWT you already have; call `SetToken` to replace it. In CI or on a SPIFFE-enabled platform, use the [GitHub Actions](githubactions.md) or [SPIFFE](spiffe.md) provider instead. For any other source, implement the interface or wrap a function with `token.IdentityTokenProviderFunc`.
46 changes: 46 additions & 0 deletions docs/conjur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Conjur Credentials Provider

Configure a [JWT authenticator](https://docs.cyberark.com/secrets-manager-sh/latest/en/content/operations/services/cjr-authn-jwt.htm) in Conjur that trusts your identity token issuer (`jwks-uri` or `provider-uri`, `issuer`, `audience`), then read variables with it. All variables are read in one batch request and published as a `*credential.Secret` under the keys you choose.

Package [`pkg/conjur`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/conjur). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```go
import (
"go.riptides.io/tokenex/pkg/conjur"
"go.riptides.io/tokenex/pkg/credential"
)

// tlsConfig may be nil; set RootCAs when Conjur's certificate is issued by a private CA.
// For Secrets Manager SaaS, use "https://<subdomain>.secretsmgr.cyberark.cloud/api" and account "conjur".
conjurProvider, err := conjur.NewCredentialsProvider(ctx, logger, "https://conjur.example.com", tlsConfig)
if err != nil {
return err
}

credsChan, err := conjurProvider.GetCredentials(
ctx,
idTokenProvider,
conjur.WithAccount("myorg"),
conjur.WithServiceID("riptides"), // authn-jwt/<service-id>
conjur.WithVariables(map[string]string{
"username": "prod/db/username",
"password": "prod/db/password",
}),
// Only needed when the authenticator has no token-app-property:
// conjur.WithHostID("apps/myapp"),
)
if err != nil {
return err
}

for creds := range credsChan {
if creds.Err != nil {
return creds.Err
}

secret := creds.Credential.(*credential.Secret)
logger.Info("Database username", "value", secret.Data["username"])
}
```

Conjur variables have no lease, so the provider reads them again every 15 minutes by default (`conjur.WithPollInterval`).
56 changes: 56 additions & 0 deletions docs/gcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# GCP Credentials Provider

Exchanges ID tokens for GCP access tokens using GCP's Workload Identity Federation.

Package [`pkg/gcp`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/gcp). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```go
import (
"go.riptides.io/tokenex/pkg/gcp"
"go.riptides.io/tokenex/pkg/credential"
)

// Create the GCP credentials provider
gcpProvider, err := gcp.NewCredentialsProvider(ctx, logger)
if err != nil {
log.Fatalf("Failed to create GCP credentials provider: %v", err)
}

// Get GCP credentials
gcpCredsChan, 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"),
)
if err != nil {
log.Fatalf("Failed to get GCP credentials: %v", err)
}

// Process credentials from the channel in a goroutine with proper context handling
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case creds, ok := <-gcpCredsChan:
if !ok {
log.Println("GCP credentials channel closed")
return
}
if creds.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
}
}
}()
```
51 changes: 51 additions & 0 deletions docs/generic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Generic Credentials Provider

Simply returns the token provided by the identity token provider and refreshes it before expiration.

Package [`pkg/generic`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/generic). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```go
import (
"go.riptides.io/tokenex/pkg/generic"
"go.riptides.io/tokenex/pkg/credential"
)

// Create the Generic credentials provider
genericProvider, err := generic.NewCredentialsProvider(ctx, logger)
if err != 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)
if err != nil {
log.Fatalf("Failed to get Generic credentials: %v", err)
}

// Process credentials from the channel in a goroutine with proper context handling
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case creds, ok := <-genericCredsChan:
if !ok {
log.Println("Generic credentials channel closed")
return
}
if creds.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
}
}
}()
```
49 changes: 49 additions & 0 deletions docs/githubactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# GitHub Actions Identity Token Provider

Fetches OIDC ID tokens from the GitHub Actions runtime, so a workflow can pass them to any provider that takes an `IdentityTokenProvider` (AWS, GCP, Azure, OCI, Vault, Conjur, Generic, RFC 7523 and RFC 8693). The job needs the `id-token: write` permission:

Package [`pkg/token/githubactions`](https://pkg.go.dev/go.riptides.io/tokenex/pkg/token/githubactions). Snippets assume the [common setup](common-setup.md); [Channel Behavior](../README.md#channel-behavior) explains the result channel.

```yaml
permissions:
id-token: write
```

The example exchanges the token for an Anthropic access token via Workload Identity Federation:

```go
import (
"go.riptides.io/tokenex/pkg/credential"
"go.riptides.io/tokenex/pkg/rfc7523"
"go.riptides.io/tokenex/pkg/token/githubactions"
)

// Reads ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN from the environment.
// Use WithRequestURL / WithRequestToken where the runner's environment isn't inherited, e.g. in a container.
idTokenProvider, err := githubactions.NewIdentityTokenProvider(
// Must match the audience the federation rule expects.
githubactions.WithAudience("api.anthropic.com"),
)
if err != nil {
log.Fatalf("new provider: %v", err)
}

credCh, err := rfc7523.GetAnthropicCredentials(ctx, logr.Discard(), idTokenProvider, rfc7523.AnthropicWIFConfig{
FederationRuleID: "fdrl_...",
OrganizationID: "...",
ServiceAccountID: "...",
WorkspaceID: "...",
})
if err != nil {
log.Fatalf("get credentials: %v", err)
}

for cred := range credCh {
if cred.Err != nil {
log.Printf("anthropic token error: %v", cred.Err)
break
}
tok := cred.Credential.(*credential.Oauth2Creds)
log.Printf("got anthropic access token, expires %s", tok.Expiry)
}
```
Loading
Loading