Skip to content

Add support to load OIDC ID Tokens from a file #1213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2025
Merged
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
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### New Features and Improvements

- Add support for OIDC ID token authentication using a file
([PR #1213](https://github.com/databricks/databricks-sdk-go/pull/1213)).
- Add support for OIDC ID token authentication using an environment variable
([PR #1215](https://github.com/databricks/databricks-sdk-go/pull/1215)).

Expand Down
4 changes: 4 additions & 0 deletions config/auth_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func buildOidcTokenCredentialStrategies(cfg *Config) []CredentialsStrategy {
return oidc.NewEnvIDTokenSource(v)
}(),
},
{
name: "file-oidc",
tokenSource: oidc.NewFileTokenSource(cfg.OIDCTokenFilepath),
},
{
name: "github-oidc",
tokenSource: &githubIDTokenSource{
Expand Down
5 changes: 4 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,11 @@ type Config struct {
// specified by this argument. This argument also holds currently selected auth.
AuthType string `name:"auth_type" env:"DATABRICKS_AUTH_TYPE" auth:"-"`

// Path to the file containing an OIDC ID token.
OIDCTokenFilepath string `name:"databricks_id_token_filepath" env:"DATABRICKS_OIDC_TOKEN_FILEPATH" auth:"file-oidc"`

// Environment variable name that contains an OIDC ID token.
OIDCTokenEnv string `name:"oidc_token_env" env:"DATABRICKS_OIDC_TOKEN_ENV" auth:"-"`
OIDCTokenEnv string `name:"oidc_token_env" env:"DATABRICKS_OIDC_TOKEN_ENV" auth:"env-oidc"`

// Skip SSL certificate verification for HTTP calls.
// Use at your own risk or for unit testing purposes.
Expand Down
23 changes: 22 additions & 1 deletion config/experimental/auth/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (fn IDTokenSourceFn) IDToken(ctx context.Context, audience string) (*IDToke
return fn(ctx, audience)
}

// NewEnvIDTokenSource returns an IDTokenSource that reads the token from
// NewEnvIDTokenSource returns an IDTokenSource that reads the IDtoken from
// environment variable env.
//
// Note that the IDTokenSource does not cache the token and will read the token
Expand All @@ -48,3 +48,24 @@ func NewEnvIDTokenSource(env string) IDTokenSource {
return &IDToken{Value: t}, nil
})
}

// NewFileTokenSource returns an IDTokenSource that reads the ID token from a
// file. The file should contain a single line with the token.
func NewFileTokenSource(path string) IDTokenSource {
return IDTokenSourceFn(func(ctx context.Context, _ string) (*IDToken, error) {
if path == "" {
return nil, fmt.Errorf("missing path")
}
t, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("file %q does not exist", path)
}
return nil, err
}
if len(t) == 0 {
return nil, fmt.Errorf("file %q is empty", path)
}
return &IDToken{Value: string(t)}, nil
})
}
98 changes: 80 additions & 18 deletions config/experimental/auth/oidc/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package oidc
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -39,58 +41,118 @@ func TestNewEnvIDTokenSource(t *testing.T) {
desc string
envName string
envValue string
audience string
want *IDToken
wantErr bool
}{
{
desc: "Success - variable set",
desc: "success",
envName: "OIDC_TEST_TOKEN_SUCCESS",
envValue: "test-token-123",
audience: "test-audience-1",
want: &IDToken{Value: "test-token-123"},
wantErr: false,
},
{
desc: "Failure - variable not set",
desc: "missing env var",
envName: "OIDC_TEST_TOKEN_MISSING",
envValue: "",
audience: "test-audience-2",
want: nil,
wantErr: true,
},
{
desc: "Failure - variable set to empty string",
desc: "empty env var",
envName: "OIDC_TEST_TOKEN_EMPTY",
envValue: "",
audience: "test-audience-3",
want: nil,
wantErr: true,
},
{
desc: "Success - different variable name",
desc: "different variable name",
envName: "ANOTHER_OIDC_TOKEN",
envValue: "another-token-456",
audience: "test-audience-4",
want: &IDToken{Value: "another-token-456"},
wantErr: false,
},
{
desc: "Success - empty audience string",
envName: "OIDC_TEST_TOKEN_NO_AUDIENCE",
envValue: "token-no-audience",
audience: "",
want: &IDToken{Value: "token-no-audience"},
wantErr: false,
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
t.Setenv(tc.envName, tc.envValue)

ts := NewEnvIDTokenSource(tc.envName)
got, gotErr := ts.IDToken(context.Background(), tc.audience)
got, gotErr := ts.IDToken(context.Background(), "")

if tc.wantErr && gotErr == nil {
t.Fatalf("IDToken() want error, got none")
}
if !tc.wantErr && gotErr != nil {
t.Fatalf("IDToken() want no error, got error: %v", gotErr)
}
if !cmp.Equal(got, tc.want) {
t.Errorf("IDToken() token = %v, want %v", got, tc.want)
}
})
}
}

type testFile struct {
filename string
filecontent string
}

func TestNewFileTokenSource(t *testing.T) {
testCases := []struct {
desc string
file *testFile // file to create
filepath string
want *IDToken
wantErr bool
}{
{
desc: "missing filepath",
file: &testFile{filename: "token", filecontent: "content"},
filepath: "",
wantErr: true,
},
{
desc: "empty file",
file: &testFile{filename: "token", filecontent: ""},
filepath: "token",
wantErr: true,
},
{
desc: "file does not exist",
filepath: "nonexistent-file",
wantErr: true,
},
{
desc: "file exists",
file: &testFile{filename: "token", filecontent: "content"},
filepath: "token",
want: &IDToken{Value: "content"},
},
}

for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
tmpDir := t.TempDir()

// Create the test file if any is given.
if tc.file != nil {
tokenFile := filepath.Join(tmpDir, tc.file.filename)
if err := os.WriteFile(tokenFile, []byte(tc.file.filecontent), 0644); err != nil {
t.Fatalf("failed to create token file: %v", err)
}
}

// Only compute the fully qualified filepath if the relative
// filepath is given.
fp := tc.filepath
if tc.filepath != "" {
fp = filepath.Join(tmpDir, tc.filepath)
}

ts := NewFileTokenSource(fp)
got, gotErr := ts.IDToken(context.Background(), "")

if tc.wantErr && gotErr == nil {
t.Fatalf("IDToken() want error, got none")
Expand Down
Loading