Skip to content

Commit 10ce848

Browse files
kbukum1Copilot
andcommitted
Extract shared OIDC JSON helpers and ForDevOps wrapper
Extract buildJSONRequest/executeRequest helpers to eliminate repeated HTTP boilerplate across OIDC token exchange functions. Refactor JFrog, Cloudsmith, and GCP providers to use the new helpers. Extract getAccessTokenForDevOps to consolidate the identical OIDC configuration check, GitHub token fetch, and provider token exchange pattern shared by all 5 ForDevOps wrappers. Azure and AWS are intentionally not refactored for JSON helpers: Azure uses form-encoded requests, AWS uses form-encoded + SigV4 signing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2803a0f commit 10ce848

2 files changed

Lines changed: 287 additions & 172 deletions

File tree

internal/oidc/actions_oidc.go

Lines changed: 127 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,74 @@ type OIDCAccessToken struct {
219219
ExpiresIn time.Duration
220220
}
221221

222+
// buildJSONRequest creates a JSON POST request with standard proxy OIDC headers
223+
// (Content-Type and User-Agent). Callers can add additional headers (e.g., Accept,
224+
// Authorization) to the returned request before passing it to executeRequest.
225+
func buildJSONRequest(ctx context.Context, requestURL string, body any) (*http.Request, error) {
226+
bodyJSON, err := json.Marshal(body)
227+
if err != nil {
228+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
229+
}
230+
231+
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, bytes.NewReader(bodyJSON))
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to create request: %w", err)
234+
}
235+
236+
req.Header.Set("Content-Type", "application/json")
237+
req.Header.Set("User-Agent", "dependabot-proxy/1.0")
238+
239+
return req, nil
240+
}
241+
242+
// executeRequest executes an HTTP request and returns the status code and raw
243+
// response body. The caller is responsible for checking the status code and
244+
// unmarshalling the body.
245+
func executeRequest(req *http.Request) (int, []byte, error) {
246+
client := &http.Client{
247+
Timeout: 10 * time.Second,
248+
}
249+
250+
resp, err := client.Do(req)
251+
if err != nil {
252+
return 0, nil, fmt.Errorf("failed to execute request: %w", err)
253+
}
254+
defer resp.Body.Close()
255+
256+
body, err := io.ReadAll(resp.Body)
257+
if err != nil {
258+
return 0, nil, fmt.Errorf("failed to read response body: %w", err)
259+
}
260+
261+
return resp.StatusCode, body, nil
262+
}
263+
264+
// getAccessTokenForDevOps is a shared wrapper for all OIDC providers' ForDevOps
265+
// functions. It checks OIDC configuration, fetches the GitHub OIDC token, and
266+
// calls the provider-specific exchange function.
267+
func getAccessTokenForDevOps(
268+
ctx context.Context,
269+
getGitHubToken func(ctx context.Context) (string, error),
270+
exchange func(ctx context.Context, githubToken string) (*OIDCAccessToken, error),
271+
providerName string,
272+
) (*OIDCAccessToken, error) {
273+
if !IsOIDCConfigured() {
274+
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
275+
}
276+
277+
githubToken, err := getGitHubToken(ctx)
278+
if err != nil {
279+
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
280+
}
281+
282+
token, err := exchange(ctx, githubToken)
283+
if err != nil {
284+
return nil, fmt.Errorf("failed to exchange GitHub token for %s token: %w", providerName, err)
285+
}
286+
287+
return token, nil
288+
}
289+
222290
// GetAzureAccessToken exchanges a GitHub Actions OIDC token for an Azure AD access token
223291
// using the OAuth 2.0 client credentials flow with federated identity credentials.
224292
// This is specifically designed for authenticating with Azure DevOps.
@@ -296,23 +364,13 @@ func GetAzureAccessToken(ctx context.Context, params AzureOIDCParameters, github
296364
// GetAzureAccessTokenForDevOps is a convenience function that combines fetching the GitHub OIDC token
297365
// and exchanging it for an Azure AD access token in a single call.
298366
func GetAzureAccessTokenForDevOps(ctx context.Context, params AzureOIDCParameters) (*OIDCAccessToken, error) {
299-
if !IsOIDCConfigured() {
300-
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
301-
}
302-
303-
// Get GitHub OIDC token
304-
githubToken, err := GetTokenForAzureADExchange(ctx)
305-
if err != nil {
306-
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
307-
}
308-
309-
// Exchange for Azure token
310-
azureToken, err := GetAzureAccessToken(ctx, params, githubToken)
311-
if err != nil {
312-
return nil, fmt.Errorf("failed to exchange GitHub token for Azure token: %w", err)
313-
}
314-
315-
return azureToken, nil
367+
return getAccessTokenForDevOps(ctx,
368+
func(ctx context.Context) (string, error) { return GetTokenForAzureADExchange(ctx) },
369+
func(ctx context.Context, githubToken string) (*OIDCAccessToken, error) {
370+
return GetAzureAccessToken(ctx, params, githubToken)
371+
},
372+
"Azure",
373+
)
316374
}
317375

318376
// GetJFrogAccessToken exchanges a GitHub Actions OIDC token for a JFrog access token
@@ -345,35 +403,18 @@ func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, github
345403
}
346404
tokenURL := fmt.Sprintf("%s/access/api/v1/oidc/token", strings.TrimSuffix(params.JFrogURL, "/"))
347405

348-
tokenRequestJson, err := json.Marshal(tokenRequest)
349-
if err != nil {
350-
return nil, fmt.Errorf("failed to marshal JFrog token request: %w", err)
351-
}
352-
353-
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewReader(tokenRequestJson))
406+
req, err := buildJSONRequest(ctx, tokenURL, tokenRequest)
354407
if err != nil {
355-
return nil, fmt.Errorf("failed to create JFrog token request: %w", err)
408+
return nil, fmt.Errorf("JFrog token request: %w", err)
356409
}
357410

358-
req.Header.Set("Content-Type", "application/json")
359-
req.Header.Set("User-Agent", "dependabot-proxy/1.0")
360-
361-
client := &http.Client{
362-
Timeout: 10 * time.Second,
363-
}
364-
resp, err := client.Do(req)
365-
if err != nil {
366-
return nil, fmt.Errorf("failed to execute JFrog token request: %w", err)
367-
}
368-
defer resp.Body.Close()
369-
370-
body, err := io.ReadAll(resp.Body)
411+
statusCode, body, err := executeRequest(req)
371412
if err != nil {
372-
return nil, fmt.Errorf("failed to read JFrog token response body: %w", err)
413+
return nil, fmt.Errorf("JFrog token request: %w", err)
373414
}
374415

375-
if resp.StatusCode != http.StatusOK {
376-
return nil, fmt.Errorf("JFrog returned status %d: %s", resp.StatusCode, string(body))
416+
if statusCode != http.StatusOK {
417+
return nil, fmt.Errorf("JFrog returned status %d: %s", statusCode, string(body))
377418
}
378419

379420
var tokenResp jfrogTokenResponse
@@ -397,23 +438,13 @@ func GetJFrogAccessToken(ctx context.Context, params JFrogOIDCParameters, github
397438
}
398439

399440
func GetJFrogAccessTokenForDevOps(ctx context.Context, params JFrogOIDCParameters) (*OIDCAccessToken, error) {
400-
if !IsOIDCConfigured() {
401-
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
402-
}
403-
404-
// Get GitHub OIDC token
405-
githubToken, err := GetToken(ctx, params.Audience)
406-
if err != nil {
407-
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
408-
}
409-
410-
// Exchange for JFrog token
411-
jfrogToken, err := GetJFrogAccessToken(ctx, params, githubToken)
412-
if err != nil {
413-
return nil, fmt.Errorf("failed to exchange GitHub token for JFrog token: %w", err)
414-
}
415-
416-
return jfrogToken, nil
441+
return getAccessTokenForDevOps(ctx,
442+
func(ctx context.Context) (string, error) { return GetToken(ctx, params.Audience) },
443+
func(ctx context.Context, githubToken string) (*OIDCAccessToken, error) {
444+
return GetJFrogAccessToken(ctx, params, githubToken)
445+
},
446+
"JFrog",
447+
)
417448
}
418449

419450
// GetAWSAccessToken exchanges a GitHub Actions OIDC token for temporary AWS credentials
@@ -573,23 +604,13 @@ func GetAWSAccessToken(ctx context.Context, params AWSOIDCParameters, githubToke
573604
}
574605

575606
func GetAWSAccessTokenForDevOps(ctx context.Context, params AWSOIDCParameters) (*OIDCAccessToken, error) {
576-
if !IsOIDCConfigured() {
577-
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
578-
}
579-
580-
// Get GitHub OIDC token
581-
githubToken, err := GetToken(ctx, params.Audience)
582-
if err != nil {
583-
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
584-
}
585-
586-
// Exchange for AWS token
587-
awsToken, err := GetAWSAccessToken(ctx, params, githubToken)
588-
if err != nil {
589-
return nil, fmt.Errorf("failed to exchange GitHub token for AWS token: %w", err)
590-
}
591-
592-
return awsToken, nil
607+
return getAccessTokenForDevOps(ctx,
608+
func(ctx context.Context) (string, error) { return GetToken(ctx, params.Audience) },
609+
func(ctx context.Context, githubToken string) (*OIDCAccessToken, error) {
610+
return GetAWSAccessToken(ctx, params, githubToken)
611+
},
612+
"AWS",
613+
)
593614
}
594615

595616
func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParameters, githubToken string) (*OIDCAccessToken, error) {
@@ -611,37 +632,20 @@ func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParamete
611632
ServiceSlug: params.ServiceSlug,
612633
}
613634

614-
requestBodyJson, err := json.Marshal(requestBody)
615-
if err != nil {
616-
return nil, fmt.Errorf("failed to marshal cloudsmith token request: %w", err)
617-
}
618-
619635
tokenURL := fmt.Sprintf("https://%s/openid/%s/", params.ApiHost, params.OrgName)
620-
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewReader(requestBodyJson))
636+
req, err := buildJSONRequest(ctx, tokenURL, requestBody)
621637
if err != nil {
622-
return nil, fmt.Errorf("failed to create cloudsmith token request: %w", err)
638+
return nil, fmt.Errorf("cloudsmith token request: %w", err)
623639
}
624-
625-
req.Header.Set("Content-Type", "application/json")
626640
req.Header.Set("Accept", "application/json")
627-
req.Header.Set("User-Agent", "dependabot-proxy/1.0")
628-
629-
client := &http.Client{
630-
Timeout: 10 * time.Second,
631-
}
632-
resp, err := client.Do(req)
633-
if err != nil {
634-
return nil, fmt.Errorf("failed to execute cloudsmith token request: %w", err)
635-
}
636-
defer resp.Body.Close()
637641

638-
body, err := io.ReadAll(resp.Body)
642+
statusCode, body, err := executeRequest(req)
639643
if err != nil {
640-
return nil, fmt.Errorf("failed to read cloudsmith token response body: %w", err)
644+
return nil, fmt.Errorf("cloudsmith token request: %w", err)
641645
}
642646

643-
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
644-
return nil, fmt.Errorf("cloudsmith returned status %d: %s", resp.StatusCode, string(body))
647+
if statusCode != http.StatusOK && statusCode != http.StatusCreated {
648+
return nil, fmt.Errorf("cloudsmith returned status %d: %s", statusCode, string(body))
645649
}
646650

647651
var tokenResp cloudsmithTokenResponse
@@ -661,22 +665,13 @@ func GetCloudsmithAccessToken(ctx context.Context, params CloudsmithOIDCParamete
661665
}
662666

663667
func GetCloudsmithAccessTokenForDevOps(ctx context.Context, params CloudsmithOIDCParameters) (*OIDCAccessToken, error) {
664-
if !IsOIDCConfigured() {
665-
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
666-
}
667-
668-
// Get GitHub OIDC token
669-
githubToken, err := GetToken(ctx, params.Audience)
670-
if err != nil {
671-
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
672-
}
673-
674-
cloudsmithToken, err := GetCloudsmithAccessToken(ctx, params, githubToken)
675-
if err != nil {
676-
return nil, fmt.Errorf("failed to exchange GitHub token for cloudsmith token: %w", err)
677-
}
678-
679-
return cloudsmithToken, nil
668+
return getAccessTokenForDevOps(ctx,
669+
func(ctx context.Context) (string, error) { return GetToken(ctx, params.Audience) },
670+
func(ctx context.Context, githubToken string) (*OIDCAccessToken, error) {
671+
return GetCloudsmithAccessToken(ctx, params, githubToken)
672+
},
673+
"cloudsmith",
674+
)
680675
}
681676

682677
func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToken string) (*OIDCAccessToken, error) {
@@ -700,36 +695,19 @@ func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToke
700695
Scope: "https://www.googleapis.com/auth/cloud-platform",
701696
}
702697

703-
stsBodyJSON, err := json.Marshal(stsReqBody)
704-
if err != nil {
705-
return nil, fmt.Errorf("failed to marshal GCP STS request: %w", err)
706-
}
707-
708-
stsReq, err := http.NewRequestWithContext(ctx, "POST", "https://sts.googleapis.com/v1/token", bytes.NewReader(stsBodyJSON))
698+
stsReq, err := buildJSONRequest(ctx, "https://sts.googleapis.com/v1/token", stsReqBody)
709699
if err != nil {
710-
return nil, fmt.Errorf("failed to create GCP STS request: %w", err)
700+
return nil, fmt.Errorf("GCP STS token exchange: %w", err)
711701
}
712-
713-
stsReq.Header.Set("Content-Type", "application/json")
714702
stsReq.Header.Set("Accept", "application/json")
715-
stsReq.Header.Set("User-Agent", "dependabot-proxy/1.0")
716703

717-
client := &http.Client{
718-
Timeout: 10 * time.Second,
719-
}
720-
stsResp, err := client.Do(stsReq)
704+
stsStatusCode, stsBody, err := executeRequest(stsReq)
721705
if err != nil {
722-
return nil, fmt.Errorf("failed to execute GCP STS request: %w", err)
706+
return nil, fmt.Errorf("GCP STS token exchange: %w", err)
723707
}
724-
defer stsResp.Body.Close()
725708

726-
stsBody, err := io.ReadAll(stsResp.Body)
727-
if err != nil {
728-
return nil, fmt.Errorf("failed to read GCP STS response body: %w", err)
729-
}
730-
731-
if stsResp.StatusCode != http.StatusOK {
732-
return nil, fmt.Errorf("GCP STS returned status %d (audience: %s): %s", stsResp.StatusCode, params.Audience, string(stsBody))
709+
if stsStatusCode != http.StatusOK {
710+
return nil, fmt.Errorf("GCP STS returned status %d (audience: %s): %s", stsStatusCode, params.Audience, string(stsBody))
733711
}
734712

735713
var stsTokenResp gcpSTSTokenResponse
@@ -758,35 +736,21 @@ func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToke
758736
Scope: []string{"https://www.googleapis.com/auth/cloud-platform"},
759737
}
760738

761-
iamBodyJSON, err := json.Marshal(iamReqBody)
762-
if err != nil {
763-
return nil, fmt.Errorf("failed to marshal GCP IAM request: %w", err)
764-
}
765-
766739
iamURL := fmt.Sprintf("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateAccessToken", params.ServiceAccount)
767-
iamReq, err := http.NewRequestWithContext(ctx, "POST", iamURL, bytes.NewReader(iamBodyJSON))
740+
iamReq, err := buildJSONRequest(ctx, iamURL, iamReqBody)
768741
if err != nil {
769-
return nil, fmt.Errorf("failed to create GCP IAM request: %w", err)
742+
return nil, fmt.Errorf("GCP IAM impersonation: %w", err)
770743
}
771-
772-
iamReq.Header.Set("Content-Type", "application/json")
773744
iamReq.Header.Set("Accept", "application/json")
774745
iamReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", stsTokenResp.AccessToken))
775-
iamReq.Header.Set("User-Agent", "dependabot-proxy/1.0")
776746

777-
iamResp, err := client.Do(iamReq)
747+
iamStatusCode, iamBody, err := executeRequest(iamReq)
778748
if err != nil {
779-
return nil, fmt.Errorf("failed to execute GCP IAM request: %w", err)
749+
return nil, fmt.Errorf("GCP IAM impersonation: %w", err)
780750
}
781-
defer iamResp.Body.Close()
782751

783-
iamBody, err := io.ReadAll(iamResp.Body)
784-
if err != nil {
785-
return nil, fmt.Errorf("failed to read GCP IAM response body: %w", err)
786-
}
787-
788-
if iamResp.StatusCode != http.StatusOK {
789-
return nil, fmt.Errorf("GCP IAM returned status %d (service-account: %s): %s", iamResp.StatusCode, params.ServiceAccount, string(iamBody))
752+
if iamStatusCode != http.StatusOK {
753+
return nil, fmt.Errorf("GCP IAM returned status %d (service-account: %s): %s", iamStatusCode, params.ServiceAccount, string(iamBody))
790754
}
791755

792756
var iamTokenResp gcpIAMGenerateAccessTokenResponse
@@ -815,22 +779,13 @@ func GetGCPAccessToken(ctx context.Context, params GCPOIDCParameters, githubToke
815779
}
816780

817781
func GetGCPAccessTokenForDevOps(ctx context.Context, params GCPOIDCParameters) (*OIDCAccessToken, error) {
818-
if !IsOIDCConfigured() {
819-
return nil, fmt.Errorf("GitHub Actions OIDC is not configured")
820-
}
821-
822-
// Get GitHub OIDC token
823-
githubToken, err := GetToken(ctx, params.Audience)
824-
if err != nil {
825-
return nil, fmt.Errorf("failed to get GitHub OIDC token: %w", err)
826-
}
827-
828-
gcpToken, err := GetGCPAccessToken(ctx, params, githubToken)
829-
if err != nil {
830-
return nil, fmt.Errorf("failed to exchange GitHub token for GCP token: %w", err)
831-
}
832-
833-
return gcpToken, nil
782+
return getAccessTokenForDevOps(ctx,
783+
func(ctx context.Context) (string, error) { return GetToken(ctx, params.Audience) },
784+
func(ctx context.Context, githubToken string) (*OIDCAccessToken, error) {
785+
return GetGCPAccessToken(ctx, params, githubToken)
786+
},
787+
"GCP",
788+
)
834789
}
835790

836791
func calculateContentSha256Header(payload []byte) string {

0 commit comments

Comments
 (0)