diff --git a/sdk/access_token_test.go b/sdk/access_token_test.go new file mode 100644 index 0000000000..50473a76fd --- /dev/null +++ b/sdk/access_token_test.go @@ -0,0 +1,73 @@ +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" +) + +func TestAccessToken_ReturnsTokenFromSource(t *testing.T) { + s := &SDK{tokenSource: FakeAccessTokenSource{accessToken: "test-token"}} + + tok, err := s.Auth().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.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) +} + +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") +} 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 01f2ae04d1..c9c02adbac 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") )