-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add workload identity federation support #49
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,14 @@ | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/url" | ||
| "os" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/conductorone/baton-sdk/pkg/uhttp" | ||
| "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" | ||
|
|
@@ -51,6 +57,95 @@ | |
| return httpClient, nil | ||
| } | ||
|
|
||
| func (o *OAuth2) Apply(req *http.Request) { | ||
| // No need to set the Authorization header here, the oauth2 client does it automatically | ||
| func (o *OAuth2) Apply(req *http.Request) {} | ||
|
|
||
| // TokenFederation authenticates via RFC 8693 token exchange. | ||
| // It presents an externally-issued JWT (e.g. SPIFFE JWT-SVID, Kubernetes SA token) | ||
| // and exchanges it for a short-lived Databricks OAuth token. | ||
| type TokenFederation struct { | ||
| clientID string | ||
| tokenURL string | ||
| tokenFile string | ||
| staticToken string | ||
| } | ||
|
|
||
| func NewTokenFederation(accId, clientId, tokenFile, token, accountHostname string) *TokenFederation { | ||
| return &TokenFederation{ | ||
| clientID: clientId, | ||
| tokenURL: fmt.Sprintf("https://%s/oidc/accounts/%s/v1/token", accountHostname, accId), | ||
| tokenFile: tokenFile, | ||
| staticToken: token, | ||
| } | ||
| } | ||
|
|
||
| func (tf *TokenFederation) readSubjectToken() (string, error) { | ||
| if tf.tokenFile != "" { | ||
| data, err := os.ReadFile(tf.tokenFile) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to read token file %s: %w", tf.tokenFile, err) | ||
| } | ||
| return strings.TrimSpace(string(data)), nil | ||
| } | ||
| return tf.staticToken, nil | ||
| } | ||
|
|
||
| // Token performs an RFC 8693 token exchange and returns the resulting OAuth2 token. | ||
| func (tf *TokenFederation) Token() (*oauth2.Token, error) { | ||
| subjectToken, err := tf.readSubjectToken() | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if subjectToken == "" { | ||
| return nil, fmt.Errorf("empty subject token for workload identity federation") | ||
| } | ||
|
|
||
| form := url.Values{ | ||
| "grant_type": {"urn:ietf:params:oauth:grant-type:token-exchange"}, | ||
| "subject_token": {subjectToken}, | ||
| "subject_token_type": {"urn:ietf:params:oauth:token-type:jwt"}, | ||
| "client_id": {tf.clientID}, | ||
| "scope": {"all-apis"}, | ||
| } | ||
|
|
||
| resp, err := http.PostForm(tf.tokenURL, form) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("token exchange request failed: %w", err) | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| body, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read token exchange response: %w", err) | ||
| } | ||
|
|
||
| if resp.StatusCode != http.StatusOK { | ||
| return nil, fmt.Errorf("token exchange failed with status %d: %s", resp.StatusCode, string(body)) | ||
| } | ||
|
|
||
| var tokenResp struct { | ||
| AccessToken string `json:"access_token"` | ||
| TokenType string `json:"token_type"` | ||
| ExpiresIn int64 `json:"expires_in"` | ||
| } | ||
| if err := json.Unmarshal(body, &tokenResp); err != nil { | ||
| return nil, fmt.Errorf("failed to decode token exchange response: %w", err) | ||
| } | ||
|
|
||
| token := &oauth2.Token{ | ||
| AccessToken: tokenResp.AccessToken, | ||
| TokenType: tokenResp.TokenType, | ||
| } | ||
| if tokenResp.ExpiresIn > 0 { | ||
| token.Expiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second) | ||
| } | ||
|
Comment on lines
+138
to
+140
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: If the token endpoint omits |
||
|
|
||
| return token, nil | ||
| } | ||
|
|
||
| func (tf *TokenFederation) GetClient(ctx context.Context) (*http.Client, error) { | ||
| ts := oauth2.ReuseTokenSource(nil, tf) | ||
| httpClient := oauth2.NewClient(ctx, ts) | ||
| return httpClient, nil | ||
| } | ||
|
|
||
| func (tf *TokenFederation) Apply(req *http.Request) {} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion:
http.PostFormuseshttp.DefaultClient, which has no timeout and ignores the caller's context. A slow or hung Databricks token endpoint will block the sync indefinitely, and context cancellation during token exchange is not honored. Consider building the request withhttp.NewRequestWithContext(ctx, ...)on anhttp.Clientwith an explicit timeout. NoteToken()currently has noctxparam — plumbingctxfromGetClient(via a bound token source) would also address R11 context propagation. (confidence: medium)