Skip to content

Add workload federation auth#134

Merged
pquerna merged 3 commits intomainfrom
pq/workload_federation_support
Feb 18, 2026
Merged

Add workload federation auth#134
pquerna merged 3 commits intomainfrom
pq/workload_federation_support

Conversation

@pquerna
Copy link
Contributor

@pquerna pquerna commented Feb 14, 2026

Support CONDUCTORONE_ACCESS_TOKEN and CONDUCTORONE_OIDC_TOKEN

Summary by CodeRabbit

  • New Features
    • Authenticate using access tokens supplied via environment variables.
    • Authenticate using OIDC tokens with automatic exchange to platform access tokens.
    • Automatic server host detection and selection from multiple configuration sources.
    • New client entry points to build clients from access tokens or OIDC tokens for simpler integration.

@coderabbitai
Copy link

coderabbitai bot commented Feb 14, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Adds host normalization/resolution, new client entry points (access-token and OIDC exchange), a token-exchange oauth2.TokenSource implementation (RFC 8693), and simplifies NewC1TokenSource to return only an oauth2.TokenSource.

Changes

Cohort / File(s) Summary
Client entrypoints & host resolution
pkg/client/client.go
Adds env auth constants, host normalization (normalizeHost, ResolveServerHost, parseClientIDName), new constructors NewWithAccessToken, NewWithOIDCToken, and newClientWithTokenSource to centralize client creation using a token source. Review: host-resolution priority and validation.
Token exchange implementation
pkg/client/token_exchange.go
New file implementing RFC 8693 token exchange as NewTokenExchangeSource; exchanges OIDC JWT for C1 access token, returns an oauth2.TokenSource with expiry handling and HTTP error parsing. Review: HTTP request/response parsing and expiry logic.
Token source signature cleanup
pkg/client/token_source.go
Removes parseClientID and simplifies NewC1TokenSource signature to return only (oauth2.TokenSource, error), eliminating returned clientName/tokenHost values. Review: callsites updated to new signature.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant NewWithOIDCToken
    participant TokenExchangeSource
    participant TokenEndpoint
    participant HTTPClient
    participant SDKClient

    Caller->>NewWithOIDCToken: Call with OIDC token, clientID, tokenHost
    NewWithOIDCToken->>NewWithOIDCToken: Resolve server host / validate inputs
    NewWithOIDCToken->>TokenExchangeSource: NewTokenExchangeSource(ctx, oidcToken, clientID, tokenHost)
    TokenExchangeSource->>HTTPClient: Build HTTP client (UA, logging, debug)
    TokenExchangeSource->>TokenEndpoint: POST token exchange (subject_token)
    TokenEndpoint-->>TokenExchangeSource: Return access_token + expiry
    TokenExchangeSource-->>NewWithOIDCToken: oauth2.TokenSource (reuse wrapper)
    NewWithOIDCToken->>SDKClient: newClientWithTokenSource(tokenSource, host, clientName)
    SDKClient-->>Caller: Initialized authenticated client
Loading
sequenceDiagram
    participant Caller
    participant NewWithAccessToken
    participant newClientWithTokenSource
    participant HTTPClient
    participant SDKClient

    Caller->>NewWithAccessToken: Call with access token, clientID
    NewWithAccessToken->>NewWithAccessToken: Resolve server host from clientID/env
    NewWithAccessToken->>newClientWithTokenSource: oauth2.StaticTokenSource(token)
    newClientWithTokenSource->>HTTPClient: Build HTTP client with token source
    newClientWithTokenSource->>SDKClient: Initialize SDK client with server URL & UA
    SDKClient-->>Caller: Return authenticated client
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I chased a URL through moonlit code,

Swapped JWTs where tokens flowed,
Hosts aligned and clients woke,
A hop, a tweak, an auth-token poke,
The meadow hums — new paths now goad.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Add workload federation auth' is somewhat vague and generic. While it references workload federation, the PR actually implements multiple authentication methods (access token, OIDC token exchange) and introduces new entry points and host resolution logic. Consider a more specific title that captures the scope, such as 'Add workload federation and token exchange authentication' or 'Support environment-based auth tokens and OIDC token exchange'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pq/workload_federation_support

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@pkg/client/client.go`:
- Around line 173-185: NewWithOIDCToken must validate inputs early: check that
oidcToken and clientID are non-empty at the top of the function and return a
clear error (e.g., fmt.Errorf) if either is missing, before calling
parseClientID or NewTokenExchangeSource; this prevents downstream token-exchange
failures and gives a fast, descriptive failure point referencing
NewWithOIDCToken, oidcToken, clientID, parseClientID, and
NewTokenExchangeSource.
- Around line 138-147: The NewWithAccessToken function currently proceeds even
when accessToken is empty, causing confusing auth failures later; update
NewWithAccessToken to validate the accessToken argument at the start (check that
accessToken is non-empty) and return a clear error immediately if it's empty
(include the function name NewWithAccessToken and parameter accessToken in the
error message) so callers get immediate, actionable feedback instead of creating
a client with an invalid token.
- Around line 149-167: The tokenHost value may contain a scheme which will
produce invalid base URLs later; update the logic around tokenHost (used with
parseClientID and v.GetString("api-endpoint")) to normalize it by parsing with
url.Parse and extracting the Host (or Path for host-only inputs), stripping any
trailing slashes, and falling back to the original value if parsing fails;
ensure the error message using EnvAccessToken and EnvClientID remains unchanged.

In `@pkg/client/token_exchange.go`:
- Around line 19-22: The two OAuth2 URN constants grantTypeTokenExchange and
subjectTokenTypeJWT are being flagged by gosec G101 as false positives; add
explicit gosec suppression comments (e.g., append `// `#nosec` G101` or a scoped
Gosec suppression) directly above or on the same line as each constant
declaration to silence the false positive while keeping the constants unchanged,
ensuring CI stops failing for these known non-credential URNs.
- Around line 33-57: The Token() method on tokenExchangeSource currently uses
context.Background() and the http client may lack a total request timeout;
update Token() to accept/use the caller context (use the ctx passed into the
tokenExchangeSource constructor or a context param) when creating the request
(replace context.Background() with that context) so cancellations and deadlines
propagate, and ensure the http.Client stored on tokenExchangeSource has an
explicit Timeout set in the constructor (set http.Client.Timeout when creating
t.httpClient) so the token exchange cannot hang indefinitely while reading the
response body; adjust any constructor that builds t.httpClient (e.g., where
uhttp.NewClient is invoked) to accept the ctx and set the Timeout accordingly.

Comment on lines +138 to +147
func NewWithAccessToken(
ctx context.Context,
accessToken string,
clientID string,
v *viper.Viper,
cmdName string,
) (C1Client, error) {
tokenSrc := oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: accessToken,
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject empty access tokens early.

This currently builds a client even if accessToken is empty, leading to confusing auth errors later.

🛠️ Proposed fix
 func NewWithAccessToken(
 	ctx context.Context,
 	accessToken string,
 	clientID string,
 	v *viper.Viper,
 	cmdName string,
 ) (C1Client, error) {
+	if accessToken == "" {
+		return nil, fmt.Errorf("%s is required", EnvAccessToken)
+	}
 	tokenSrc := oauth2.StaticTokenSource(&oauth2.Token{
 		AccessToken: accessToken,
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewWithAccessToken(
ctx context.Context,
accessToken string,
clientID string,
v *viper.Viper,
cmdName string,
) (C1Client, error) {
tokenSrc := oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: accessToken,
})
func NewWithAccessToken(
ctx context.Context,
accessToken string,
clientID string,
v *viper.Viper,
cmdName string,
) (C1Client, error) {
if accessToken == "" {
return nil, fmt.Errorf("%s is required", EnvAccessToken)
}
tokenSrc := oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: accessToken,
})
🤖 Prompt for AI Agents
In `@pkg/client/client.go` around lines 138 - 147, The NewWithAccessToken function
currently proceeds even when accessToken is empty, causing confusing auth
failures later; update NewWithAccessToken to validate the accessToken argument
at the start (check that accessToken is non-empty) and return a clear error
immediately if it's empty (include the function name NewWithAccessToken and
parameter accessToken in the error message) so callers get immediate, actionable
feedback instead of creating a client with an invalid token.

Comment on lines +33 to +57
func (t *tokenExchangeSource) Token() (*oauth2.Token, error) {
body := url.Values{
"grant_type": []string{grantTypeTokenExchange},
"subject_token": []string{t.oidcToken},
"subject_token_type": []string{subjectTokenTypeJWT},
"client_id": []string{t.clientID},
}

tokenURL := url.URL{
Scheme: "https",
Host: t.tokenHost,
Path: "auth/v1/token",
}

req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, tokenURL.String(), strings.NewReader(body.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := t.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "NewClient|NewTransport|Timeout|ResponseHeaderTimeout|IdleConnTimeout" pkg/uhttp -C 3

Repository: ConductorOne/cone

Length of output: 1939


🏁 Script executed:

cat -n pkg/uhttp/client.go | head -130

Repository: ConductorOne/cone

Length of output: 3153


Propagate caller context and ensure timeout for token exchange requests.

Token() uses context.Background(), ignoring caller cancellations/timeouts. Additionally, uhttp.NewClient does not set http.Client.Timeout, only transport-level phase timeouts (dial, TLS handshake). This means requests can hang indefinitely while reading the response body. Wire the constructor's ctx through and add an explicit timeout to the http.Client.

🛠️ Proposed fix
 type tokenExchangeSource struct {
+	ctx        context.Context
 	oidcToken  string
 	clientID   string
 	tokenHost  string
 	httpClient *http.Client
 }
 
 func (t *tokenExchangeSource) Token() (*oauth2.Token, error) {
+	ctx := t.ctx
+	if ctx == nil {
+		ctx = context.Background()
+	}
 	body := url.Values{
 		"grant_type":         []string{grantTypeTokenExchange},
 		"subject_token":      []string{t.oidcToken},
 		"subject_token_type": []string{subjectTokenTypeJWT},
 		"client_id":          []string{t.clientID},
 	}
 
 	tokenURL := url.URL{
 		Scheme: "https",
 		Host:   t.tokenHost,
 		Path:   "auth/v1/token",
 	}
 
-	req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, tokenURL.String(), strings.NewReader(body.Encode()))
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL.String(), strings.NewReader(body.Encode()))
 	if err != nil {
 		return nil, err
 	}
 func NewTokenExchangeSource(ctx context.Context, oidcToken, clientID, tokenHost string, debug bool) (oauth2.TokenSource, error) {
+	if ctx == nil {
+		ctx = context.Background()
+	}
 	httpClient, err := uhttp.NewClient(ctx,
 		uhttp.WithLogger(true, ctxzap.Extract(ctx)),
 		uhttp.WithUserAgent("cone-wfe-credential-provider"),
 		uhttp.WithDebug(debug),
 	)
 	if err != nil {
 		return nil, err
 	}
+	httpClient.Timeout = 30 * time.Second
 
 	return oauth2.ReuseTokenSource(nil, &tokenExchangeSource{
+		ctx:        ctx,
 		oidcToken:  oidcToken,
 		clientID:   clientID,
 		tokenHost:  tokenHost,
 		httpClient: httpClient,
 	}), nil
 }
🤖 Prompt for AI Agents
In `@pkg/client/token_exchange.go` around lines 33 - 57, The Token() method on
tokenExchangeSource currently uses context.Background() and the http client may
lack a total request timeout; update Token() to accept/use the caller context
(use the ctx passed into the tokenExchangeSource constructor or a context param)
when creating the request (replace context.Background() with that context) so
cancellations and deadlines propagate, and ensure the http.Client stored on
tokenExchangeSource has an explicit Timeout set in the constructor (set
http.Client.Timeout when creating t.httpClient) so the token exchange cannot
hang indefinitely while reading the response body; adjust any constructor that
builds t.httpClient (e.g., where uhttp.NewClient is invoked) to accept the ctx
and set the Timeout accordingly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/client/client.go (1)

199-217: ⚠️ Potential issue | 🟠 Major

Fail fast when server host cannot be resolved in New().

ResolveServerHost can return an empty host (e.g., missing client ID and api-endpoint). That currently flows into client creation, yielding invalid base URLs later. Add a guard like the other entry points.

🛠️ Proposed fix
 func New(
 	ctx context.Context,
 	clientId string,
 	clientSecret string,
 	v *viper.Viper,
 	cmdName string,
 ) (C1Client, error) {
 	clientName, tokenHost, err := ResolveServerHost(clientId, v)
 	if err != nil {
 		return nil, err
 	}
+	if tokenHost == "" {
+		return nil, fmt.Errorf("%s requires --client-id, %s, or --api-endpoint to determine the server", EnvClientSecret, EnvClientID)
+	}
 
 	tokenSrc, err := NewC1TokenSource(ctx, clientId, clientSecret, tokenHost, v.GetBool("debug"))
 	if err != nil {
 		return nil, err
 	}

@pquerna pquerna merged commit 9307b21 into main Feb 18, 2026
3 checks passed
@pquerna pquerna deleted the pq/workload_federation_support branch February 18, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants