From cd24a2ec3d87a124fbc5fdcb78fb06e0fbd9da82 Mon Sep 17 00:00:00 2001 From: Krish Suchak Date: Wed, 22 Jul 2026 17:27:22 -0400 Subject: [PATCH 1/3] feat(sdk): add AccessToken accessor to SDK Expose a public AccessToken method on the SDK that returns a valid access token from the configured token source, passing the SDK's own HTTP client. Returns ErrNoAccessTokenSource when the SDK was created without credentials (nil token source) instead of panicking. Signed-off-by: Krish Suchak --- sdk/access_token_test.go | 26 ++++++++++++++++++++++++++ sdk/sdk.go | 10 ++++++++++ 2 files changed, 36 insertions(+) create mode 100644 sdk/access_token_test.go diff --git a/sdk/access_token_test.go b/sdk/access_token_test.go new file mode 100644 index 0000000000..ea0f7ac6b7 --- /dev/null +++ b/sdk/access_token_test.go @@ -0,0 +1,26 @@ +package sdk + +import ( + "context" + "testing" + + "github.com/opentdf/platform/sdk/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAccessToken_ReturnsTokenFromSource(t *testing.T) { + s := &SDK{tokenSource: FakeAccessTokenSource{accessToken: "test-token"}} + + tok, err := s.AccessToken(context.Background()) + require.NoError(t, err) + assert.Equal(t, auth.AccessToken("test-token"), tok) +} + +func TestAccessToken_NoTokenSource(t *testing.T) { + s := &SDK{} + + tok, err := s.AccessToken(context.Background()) + require.ErrorIs(t, err, ErrNoAccessTokenSource) + assert.Empty(t, tok) +} diff --git a/sdk/sdk.go b/sdk/sdk.go index 01f2ae04d1..cb9ab605e3 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -45,6 +45,7 @@ const ( ErrPlatformTokenEndpointNotFound = Error("token_endpoint not found in well-known idp configuration") ErrPlatformEndpointNotFound = Error("platform_endpoint not found in well-known configuration") ErrAccessTokenInvalid = Error("access token is invalid") + ErrNoAccessTokenSource = Error("no access token source configured; SDK was created without credentials") ErrWellKnowConfigEmpty = Error("well-known configuration is empty") ErrAttributeNotFound = Error("attribute not found") ) @@ -305,6 +306,15 @@ func (s SDK) Close() error { return nil } +// AccessToken returns a valid access token for the SDK's configured credentials. +// It returns ErrNoAccessTokenSource if the SDK was constructed without credentials. +func (s *SDK) AccessToken(ctx context.Context) (auth.AccessToken, error) { + if s.tokenSource == nil { + return "", ErrNoAccessTokenSource + } + return s.tokenSource.AccessToken(ctx, s.httpClient) +} + // Logger returns the configured slog.Logger for this SDK instance func (s SDK) Logger() *slog.Logger { return s.logger From 049718f3834fd909a350302e58a5ea532573460c Mon Sep 17 00:00:00 2001 From: Krish Suchak Date: Thu, 23 Jul 2026 17:15:49 -0400 Subject: [PATCH 2/3] refactor(sdk): nest AccessToken behind Auth() accessor Move the top-level SDK.AccessToken method behind an Auth() accessor (s.Auth().AccessToken(ctx)) via a new AuthClient type to keep the SDK client's public surface lean. Also gate an empty token returned by the source, returning ErrAccessTokenInvalid. Addresses PR review feedback. Signed-off-by: Krish Suchak --- sdk/access_token_test.go | 12 ++++++++++-- sdk/auth_client.go | 34 ++++++++++++++++++++++++++++++++++ sdk/sdk.go | 9 --------- 3 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 sdk/auth_client.go diff --git a/sdk/access_token_test.go b/sdk/access_token_test.go index ea0f7ac6b7..39b2c805c6 100644 --- a/sdk/access_token_test.go +++ b/sdk/access_token_test.go @@ -12,7 +12,7 @@ import ( func TestAccessToken_ReturnsTokenFromSource(t *testing.T) { s := &SDK{tokenSource: FakeAccessTokenSource{accessToken: "test-token"}} - tok, err := s.AccessToken(context.Background()) + tok, err := s.Auth().AccessToken(context.Background()) require.NoError(t, err) assert.Equal(t, auth.AccessToken("test-token"), tok) } @@ -20,7 +20,15 @@ func TestAccessToken_ReturnsTokenFromSource(t *testing.T) { func TestAccessToken_NoTokenSource(t *testing.T) { s := &SDK{} - tok, err := s.AccessToken(context.Background()) + tok, err := s.Auth().AccessToken(context.Background()) require.ErrorIs(t, err, ErrNoAccessTokenSource) assert.Empty(t, tok) } + +func TestAccessToken_EmptyToken(t *testing.T) { + s := &SDK{tokenSource: FakeAccessTokenSource{accessToken: ""}} + + tok, err := s.Auth().AccessToken(context.Background()) + require.ErrorIs(t, err, ErrAccessTokenInvalid) + assert.Empty(t, tok) +} diff --git a/sdk/auth_client.go b/sdk/auth_client.go new file mode 100644 index 0000000000..801e40ce0f --- /dev/null +++ b/sdk/auth_client.go @@ -0,0 +1,34 @@ +package sdk + +import ( + "context" + + "github.com/opentdf/platform/sdk/auth" +) + +// Auth returns an AuthClient for authentication-related operations. +func (s *SDK) Auth() *AuthClient { + return &AuthClient{sdk: s} +} + +// AuthClient groups authentication operations for the SDK. +type AuthClient struct { + sdk *SDK +} + +// AccessToken returns a valid access token for the SDK's configured credentials. +// It returns ErrNoAccessTokenSource if the SDK was created without credentials, and +// ErrAccessTokenInvalid if the token source returns an empty token. +func (a *AuthClient) AccessToken(ctx context.Context) (auth.AccessToken, error) { + if a.sdk.tokenSource == nil { + return "", ErrNoAccessTokenSource + } + token, err := a.sdk.tokenSource.AccessToken(ctx, a.sdk.httpClient) + if err != nil { + return "", err + } + if token == "" { + return "", ErrAccessTokenInvalid + } + return token, nil +} diff --git a/sdk/sdk.go b/sdk/sdk.go index cb9ab605e3..c9c02adbac 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -306,15 +306,6 @@ func (s SDK) Close() error { return nil } -// AccessToken returns a valid access token for the SDK's configured credentials. -// It returns ErrNoAccessTokenSource if the SDK was constructed without credentials. -func (s *SDK) AccessToken(ctx context.Context) (auth.AccessToken, error) { - if s.tokenSource == nil { - return "", ErrNoAccessTokenSource - } - return s.tokenSource.AccessToken(ctx, s.httpClient) -} - // Logger returns the configured slog.Logger for this SDK instance func (s SDK) Logger() *slog.Logger { return s.logger From 68bcc4ef59b367102b5109cdb3bcc29a9ed2527f Mon Sep 17 00:00:00 2001 From: Krish Suchak Date: Thu, 23 Jul 2026 17:24:16 -0400 Subject: [PATCH 3/3] test(sdk): assert AccessToken forwards context and client Add a recording token source that verifies Auth().AccessToken forwards the context (via a propagated context value) and the exact httpClient instance to the underlying token source. Addresses PR review feedback. Signed-off-by: Krish Suchak --- sdk/access_token_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/sdk/access_token_test.go b/sdk/access_token_test.go index 39b2c805c6..50473a76fd 100644 --- a/sdk/access_token_test.go +++ b/sdk/access_token_test.go @@ -2,8 +2,10 @@ package sdk import ( "context" + "net/http" "testing" + "github.com/lestrrat-go/jwx/v2/jwk" "github.com/opentdf/platform/sdk/auth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -32,3 +34,40 @@ func TestAccessToken_EmptyToken(t *testing.T) { require.ErrorIs(t, err, ErrAccessTokenInvalid) assert.Empty(t, tok) } + +type recordCtxKey struct{} + +// recordingTokenSource captures what AccessToken forwards to it so tests can assert +// the context and HTTP client are passed through. It records a context value rather +// than the context itself (avoiding a context.Context struct field) and the client +// pointer for identity comparison. +type recordingTokenSource struct { + token string + gotCtxValue any + gotClient *http.Client +} + +func (r *recordingTokenSource) AccessToken(ctx context.Context, client *http.Client) (auth.AccessToken, error) { + r.gotCtxValue = ctx.Value(recordCtxKey{}) + r.gotClient = client + return auth.AccessToken(r.token), nil +} + +func (r *recordingTokenSource) MakeToken(func(jwk.Key) ([]byte, error)) ([]byte, error) { + return nil, nil +} + +func TestAccessToken_ForwardsContextAndClient(t *testing.T) { + ctx := context.WithValue(context.Background(), recordCtxKey{}, "value") + client := &http.Client{} + + rec := &recordingTokenSource{token: "test-token"} + s := &SDK{tokenSource: rec} + s.httpClient = client + + tok, err := s.Auth().AccessToken(ctx) + require.NoError(t, err) + assert.Equal(t, auth.AccessToken("test-token"), tok) + assert.Equal(t, "value", rec.gotCtxValue, "context should be forwarded unchanged") + assert.Same(t, client, rec.gotClient, "http client should be forwarded unchanged") +}