sts

package
v0.1.104 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: Apache-2.0 Imports: 18 Imported by: 8

Documentation

Overview

Package sts provides OIDC token exchange functionality for Chainguard services.

Overview

The sts package implements secure token exchange (STS) for converting third-party OIDC tokens into Chainguard-issued tokens. It supports both standard gRPC-based exchanges and HTTP/1.1 downgrade mode for compatibility with environments that require HTTP/1.1.

Features

  • OIDC token exchange with Chainguard issuers
  • Refresh token support for long-lived sessions
  • Configurable scopes and capabilities
  • Identity and identity provider selection
  • HTTP/1.1 downgrade mode for compatibility
  • oauth2.TokenSource integration for seamless authentication

Basic Usage

The simplest way to exchange a token is using the ExchangePair function:

ctx := context.Background()
tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken)
if err != nil {
	log.Fatal(err)
}
// Use tokenPair.AccessToken for authenticated requests

Exchanger Interface

For more control, create an Exchanger instance with New():

exchanger := sts.New(issuer, audience)
tokenPair, err := exchanger.Exchange(ctx, idToken)
if err != nil {
	log.Fatal(err)
}

Refresh Tokens

Exchange refresh tokens for new access tokens:

accessToken, refreshToken, err := exchanger.Refresh(ctx, oldRefreshToken)
if err != nil {
	log.Fatal(err)
}

Customization Options

Configure the exchanger with options:

exchanger := sts.New(issuer, audience,
	sts.WithScope("read", "write"),
	sts.WithCapabilities("groups.list"),
	sts.WithIdentity(identityUID),
	sts.WithUserAgent("my-app/1.0"),
)

HTTP/1.1 Downgrade Mode

For environments requiring HTTP/1.1:

exchanger := sts.NewHTTP1DowngradeExchanger(issuer, audience)
tokenPair, err := exchanger.Exchange(ctx, idToken)

Or use the WithHTTP1Downgrade option:

tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken,
	sts.WithHTTP1Downgrade(),
)

oauth2 Integration

Wrap an existing oauth2.TokenSource to automatically exchange tokens:

baseTokenSource := oauth2.StaticTokenSource(&oauth2.Token{
	AccessToken: thirdPartyToken,
})
exchanger := sts.New(issuer, audience)
chainguardTokenSource := sts.NewContextTokenSource(ctx, baseTokenSource, exchanger)

// Use with any oauth2-compatible client
client := oauth2.NewClient(ctx, chainguardTokenSource)

Integration Patterns

The package integrates with standard Go authentication patterns:

  • Use with google.golang.org/grpc/credentials/oauth for gRPC authentication
  • Use with golang.org/x/oauth2 for HTTP client authentication
  • Combine with token caching for improved performance
  • Chain with other TokenSource implementations for multi-stage authentication

Error Handling

All functions return descriptive errors that can be inspected and wrapped:

tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken)
if err != nil {
	return fmt.Errorf("exchanging token: %w", err)
}

Thread Safety

Exchanger instances are safe for concurrent use. Multiple goroutines can call Exchange and Refresh on the same Exchanger instance simultaneously.

Example

Example demonstrates basic token exchange.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
	fmt.Printf("Refresh token: %s\n", tokenPair.RefreshToken)
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Exchange deprecated added in v0.1.2

func Exchange(ctx context.Context, issuer, audience, idToken string, opts ...ExchangerOption) (string, error)

Exchange performs an OIDC token exchange with the correct Exchanger based on the provided options.

Deprecated: use ExchangePair instead. This is kept around only until we migrate all existing callers to ExchangePair.

func NewContextTokenSource added in v0.1.42

func NewContextTokenSource(ctx context.Context, ts oauth2.TokenSource, xchg Exchanger) oauth2.TokenSource

NewTokenSource creates an oauth2.TokenSource by wrapping another TokenSource in a Chainguard STS exchange brokered by the provided Exchanger.

Example

ExampleNewContextTokenSource demonstrates wrapping a TokenSource with context.

package main

import (
	"context"

	"chainguard.dev/sdk/sts"
	"golang.org/x/oauth2"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	// Base token source
	baseTokenSource := oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: "third-party-token",
	})

	// Create exchanger
	exchanger := sts.New(issuer, audience)

	// Wrap with context
	chainguardTokenSource := sts.NewContextTokenSource(ctx, baseTokenSource, exchanger)

	// Use with HTTP client
	client := oauth2.NewClient(ctx, chainguardTokenSource)
	_ = client
}

func NewTokenSource

func NewTokenSource(ts oauth2.TokenSource, xchg Exchanger) oauth2.TokenSource

NewTokenSource creates an oauth2.TokenSource by wrapping another TokenSource in a Chainguard STS exchange brokered by the provided Exchanger. This wraps NewContextTokenSource with a new background context.

Example

ExampleNewTokenSource demonstrates wrapping a TokenSource for automatic exchange.

package main

import (
	"context"

	"chainguard.dev/sdk/sts"
	"golang.org/x/oauth2"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	// Base token source (e.g., from a third-party provider)
	baseTokenSource := oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: "third-party-token",
	})

	// Create exchanger
	exchanger := sts.New(issuer, audience)

	// Wrap the base token source
	chainguardTokenSource := sts.NewTokenSource(baseTokenSource, exchanger)

	// Use with any oauth2-compatible client
	ctx := context.Background()
	client := oauth2.NewClient(ctx, chainguardTokenSource)

	// The client will automatically exchange tokens when needed
	_ = client
}

func NewTokenSourceFromValues added in v0.1.33

func NewTokenSourceFromValues(ctx context.Context, issuer string, audience string, identity string, ts oauth2.TokenSource) oauth2.TokenSource

NewTokenSourceFromValues creates a new TokenSource with common input parameters. This is a convenience wrapper around NewContextTokenSource.

Example

ExampleNewTokenSourceFromValues demonstrates the convenience function for creating a TokenSource.

package main

import (
	"context"

	"chainguard.dev/sdk/sts"
	"golang.org/x/oauth2"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"
	identity := "my-identity-uid"

	// Base token source
	baseTokenSource := oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: "third-party-token",
	})

	// Create token source with common parameters
	chainguardTokenSource := sts.NewTokenSourceFromValues(
		ctx, issuer, audience, identity, baseTokenSource,
	)

	// Use with HTTP client
	client := oauth2.NewClient(ctx, chainguardTokenSource)
	_ = client
}

Types

type Exchanger

type Exchanger interface {

	// Exchange performs the actual token exchange, sending "token" to the
	// Chainguard issuer's STS interface, and receiving bytes or an error.
	Exchange(ctx context.Context, token string, opts ...ExchangerOption) (TokenPair, error)

	// Refresh exchanges a refresh token for a new access token and refresh token.
	Refresh(ctx context.Context, token string, opts ...ExchangerOption) (accessToken string, refreshToken string, err error)
}

Exchanger is an interface for exchanging a third-party token for a Chainguard token.

func New

func New(issuer, audience string, opts ...ExchangerOption) Exchanger

New creates a new Exchanger that works against the provided issuer's STS endpoint, and requests tokens with the specified audience(s). The audience parameter may contain multiple comma-separated audience values. Its behavior can be further customized via optional ExchangerOption parameters.

Example

ExampleNew demonstrates creating an Exchanger instance.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}
Example (WithOptions)

ExampleNew_withOptions demonstrates creating an Exchanger with options.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithScope("read", "write"),
		sts.WithCapabilities("groups.list", "roles.list"),
		sts.WithIdentity("my-identity-uid"),
		sts.WithUserAgent("my-app/1.0.0"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

type ExchangerOption

type ExchangerOption func(*options)

ExchangerOption is a way of customizing the behavior of the Exchanger constructed via New()

func WithCapabilities

func WithCapabilities(capabilities ...string) ExchangerOption

WithCapabilities sets the capabilities sent by the Exchanger.

Example

ExampleWithCapabilities demonstrates using the WithCapabilities option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithCapabilities("groups.list", "roles.list", "policies.read"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func WithHTTP1Downgrade added in v0.1.2

func WithHTTP1Downgrade() ExchangerOption

WithHTTP1Downgrade signals Exchange to use HTTP1DowngradeExchanger in the STS exchange.

Example

ExampleWithHTTP1Downgrade demonstrates using the WithHTTP1Downgrade option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken,
		sts.WithHTTP1Downgrade(),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func WithIdentity

func WithIdentity(uid string) ExchangerOption

WithIdentity sets the the unique ID of the identity so that STS exchange can look up pre-stored verification keys without ambiguity

Example

ExampleWithIdentity demonstrates using the WithIdentity option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithIdentity("my-identity-uid"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func WithIdentityProvider added in v0.1.25

func WithIdentityProvider(idp string) ExchangerOption

WithIdentityProvider sets the identity provider to use for the exchange.

Example

ExampleWithIdentityProvider demonstrates using the WithIdentityProvider option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithIdentityProvider("my-identity-provider-uid"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func WithScope

func WithScope(scope ...string) ExchangerOption

WithScope sets the scope parameter sent by the Exchanger.

Only one of cluster or scope may be set.

Example

ExampleWithScope demonstrates using the WithScope option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithScope("read", "write", "admin"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func WithUserAgent

func WithUserAgent(agent string) ExchangerOption

WithUserAgent sets the user agent sent by the Exchanger.

Example

ExampleWithUserAgent demonstrates using the WithUserAgent option.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.New(issuer, audience,
		sts.WithUserAgent("my-application/1.0.0"),
	)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

type HTTP1DowngradeExchanger

type HTTP1DowngradeExchanger struct {
	// contains filtered or unexported fields
}

func NewHTTP1DowngradeExchanger

func NewHTTP1DowngradeExchanger(issuer, audience string, opts ...ExchangerOption) *HTTP1DowngradeExchanger
Example

ExampleNewHTTP1DowngradeExchanger demonstrates creating an HTTP/1.1 exchanger.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"

	exchanger := sts.NewHTTP1DowngradeExchanger(issuer, audience)

	ctx := context.Background()
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := exchanger.Exchange(ctx, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

func (*HTTP1DowngradeExchanger) Exchange

func (i *HTTP1DowngradeExchanger) Exchange(ctx context.Context, token string, opts ...ExchangerOption) (TokenPair, error)

func (*HTTP1DowngradeExchanger) Refresh added in v0.1.11

func (i *HTTP1DowngradeExchanger) Refresh(ctx context.Context, token string, opts ...ExchangerOption) (string, string, error)

type TokenPair added in v0.1.25

type TokenPair struct {
	AccessToken  string //nolint:gosec // G117: struct field name for STS token response, not a hardcoded secret
	RefreshToken string //nolint:gosec // G117: struct field name for STS token response, not a hardcoded secret
	Expiry       time.Time
}

func ExchangePair added in v0.1.25

func ExchangePair(ctx context.Context, issuer, audience, idToken string, exchangerOptions ...ExchangerOption) (TokenPair, error)

ExchangePair performs an OIDC token exchange with the correct Exchanger based on the provided options.

Example

ExampleExchangePair demonstrates the convenience function for token exchange.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}
Example (WithOptions)

ExampleExchangePair_withOptions demonstrates ExchangePair with options.

package main

import (
	"context"
	"fmt"
	"log"

	"chainguard.dev/sdk/sts"
)

func main() {
	ctx := context.Background()
	issuer := "https://issuer.example.com"
	audience := "https://audience.example.com"
	idToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."

	tokenPair, err := sts.ExchangePair(ctx, issuer, audience, idToken,
		sts.WithScope("read"),
		sts.WithCapabilities("groups.list"),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Access token: %s\n", tokenPair.AccessToken)
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL