Skip to content
Open
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: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/mediacdn/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers

# Other Owners
/auth/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers @GoogleCloudPlatform/googleapis-auth
/auth/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers @GoogleCloudPlatform/googleapis-auth @GoogleCloudPlatform/aion-sdk
/asset/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers @GoogleCloudPlatform/cloud-asset-analysis-team
/billing/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers @GoogleCloudPlatform/billing-samples-maintainers
/dlp/ @GoogleCloudPlatform/go-samples-reviewers @GoogleCloudPlatform/cloud-samples-reviewers @GoogleCloudPlatform/googleapis-dlp
Expand Down
107 changes: 107 additions & 0 deletions auth/custom_credential_supplier/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Running the Custom Credential Supplier Sample

If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the [google-cloud-go/auth](https://github.com/vverman/google-cloud-go/tree/main/auth) library, a custom AwsSecurityCredentialsProvider implementation may be specified when creating an AWS client. The supplier must return valid, unexpired AWS security credentials when called by the GCP credential. Currently, using ADC with your AWS workloads is only supported with EC2. An example of a good use case for using a custom credential suppliers is when your workloads are running in other AWS environments, such as ECS, EKS, Fargate, etc.


This document provides instructions on how to run the custom credential supplier sample in different environments.

## Running Locally

To run the sample on your local system, you need to configure your AWS and GCP credentials as environment variables.

```bash
export AWS_ACCESS_KEY_ID="YOUR_AWS_ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET_ACCESS_KEY"
export GCP_WORKLOAD_AUDIENCE="YOUR_GCP_WORKLOAD_AUDIENCE"
export GCS_BUCKET_NAME="YOUR_GCS_BUCKET_NAME"

# Optional: If you want to use service account impersonation
export GCP_SERVICE_ACCOUNT_IMPERSONATION_URL="YOUR_GCP_SERVICE_ACCOUNT_IMPERSONATION_URL"

go run .
```

## Running in a Containerized Environment (EKS)

This section provides a brief overview of how to run the sample in an Amazon EKS cluster.

### 1. EKS Cluster Setup

First, you need an EKS cluster. You can create one using `eksctl` or the AWS Management Console. For detailed instructions, refer to the [Amazon EKS documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html).

### 2. Configure IAM Roles for Service Accounts (IRSA)

IRSA allows you to associate an IAM role with a Kubernetes service account. This provides a secure way for your pods to access AWS services.

- Create an IAM OIDC provider for your cluster.
- Create an IAM role and policy that grants the necessary AWS permissions.
- Associate the IAM role with a Kubernetes service account.

For detailed steps, see the [IAM Roles for Service Accounts](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) documentation.

### 3. Configure GCP to Trust the AWS Role

You need to configure your GCP project to trust the AWS IAM role you created. This is done by creating a Workload Identity Pool and Provider in GCP.

- Create a Workload Identity Pool.
- Create a Workload Identity Provider that trusts the AWS role ARN.
- Grant the GCP service account the necessary permissions.

### 4. Containerize and Package the Application

Build a Docker image of the Go application and push it to a container registry (e.g., Amazon ECR) that your EKS cluster can access.

```Dockerfile
FROM golang:1.21

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

CMD ["go", "run", "./aws"]
```

### 5. Deploy to EKS

Create a Kubernetes deployment manifest (`pod.yaml`) to deploy your application to the EKS cluster.

```yaml
apiVersion: v1
kind: Pod
metadata:
name: custom-credential-pod
spec:
serviceAccountName: your-k8s-service-account # The service account associated with the IAM role
containers:
- name: gcp-auth-sample
image: your-container-image:latest # Your image from ECR
env:
- name: AWS_REGION
value: "your-aws-region"
- name: GCP_WORKLOAD_AUDIENCE
value: "your-gcp-workload-audience"
- name: GOOGLE_CLOUD_PROJECT
value: "your-google-cloud-project"
# Optional: If you want to use service account impersonation
# - name: GCP_SERVICE_ACCOUNT_IMPERSONATION_URL
# value: "your-gcp-service-account-impersonation-url"
- name: GCS_BUCKET_NAME
value: "your-gcs-bucket-name"
```

Deploy the pod:

```bash
kubectl apply -f pod.yaml
```

### 6. Clean Up

To clean up the resources, delete the EKS cluster and any other AWS and GCP resources you created.

```bash
eksctl delete cluster --name your-cluster-name
```
175 changes: 175 additions & 0 deletions auth/custom_credential_supplier/aws/aws_custom_credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

// [START auth_custom_credential_supplier_aws]
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"

"cloud.google.com/go/auth/credentials/externalaccount"
"github.com/aws/aws-sdk-go-v2/config"
)

// customAwsSupplier implements externalaccount.AwsSecurityCredentialsProvider
// using the official AWS SDK for Go v2.
type customAwsSupplier struct{}

// AwsRegion resolves the AWS region using the AWS SDK's default configuration chain.
// It prioritizes the AWS_REGION environment variable to match standard behavior.
func (s *customAwsSupplier) AwsRegion(ctx context.Context, opts *externalaccount.RequestOptions) (string, error) {
// Explicitly check environment variable first.
if region := os.Getenv("AWS_REGION"); region != "" {
return region, nil
}
if region := os.Getenv("AWS_DEFAULT_REGION"); region != "" {
return region, nil
}

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("AWS SDK failed to load config for region: %w", err)
}

if cfg.Region == "" {
return "", fmt.Errorf("AWS region could not be resolved by SDK; ensure AWS_REGION is set")
}

return cfg.Region, nil
}

// AwsSecurityCredentials retrieves credentials using the AWS SDK's default provider chain.
func (s *customAwsSupplier) AwsSecurityCredentials(ctx context.Context, opts *externalaccount.RequestOptions) (*externalaccount.AwsSecurityCredentials, error) {
// Load the default AWS configuration.
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, fmt.Errorf("AWS SDK failed to load config for credentials: %w", err)
}

// Retrieve the actual credentials values.
creds, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("AWS SDK failed to retrieve credentials: %w", err)
}

return &externalaccount.AwsSecurityCredentials{
AccessKeyID: creds.AccessKeyID,
SecretAccessKey: creds.SecretAccessKey,
SessionToken: creds.SessionToken,
}, nil
}

// authenticateWithAwsCredentials demonstrates how to use a custom AWS credential supplier
// to authenticate with Google Cloud and verify access to a specific bucket.
//
// impersonationURL is optional. If provided, the credential will exchange the federated
// token for a Service Account token. If empty, the federated token is used directly.
func authenticateWithAwsCredentials(w io.Writer, bucketName, audience, impersonationURL string) error {
// bucketName := "sample-bucket"
// audience := "//iam.googleapis.com/projects/sample-project/locations/global/workloadIdentityPools/sample-pool/providers/sample-provider"
// [Optional] impersonationURL := "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/[email protected]:generateAccessToken"

ctx := context.Background()

// 1. Initialize Custom AWS Supplier
supplier := &customAwsSupplier{}

// 2. Configure the credentials options
opts := &externalaccount.Options{
Audience: audience,
SubjectTokenType: "urn:ietf:params:aws:token-type:aws4_request",
ServiceAccountImpersonationURL: impersonationURL,
AwsSecurityCredentialsProvider: supplier,
Scopes: []string{"https://www.googleapis.com/auth/devstorage.read_write"},
}

// 3. Create the credentials object
creds, err := externalaccount.NewCredentials(opts)
if err != nil {
return fmt.Errorf("externalaccount.NewCredentials: %w", err)
}

// 4. Authenticate and Check Bucket Existence via Raw HTTP
bucketURL := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s", bucketName)
fmt.Fprintf(w, "Request URL: %s\n", bucketURL)
fmt.Fprintln(w, "Attempting to make authenticated request to Google Cloud Storage...")

// Retrieve a valid token
token, err := creds.Token(ctx)
if err != nil {
return fmt.Errorf("creds.Token: %w", err)
}

// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "GET", bucketURL, nil)
if err != nil {
return fmt.Errorf("http.NewRequest: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token.Value)

// Execute request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("client.Do: %w", err)
}
defer resp.Body.Close()

// Check for failure
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("bucket '%s' does not exist (404)", bucketName)
}
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("request returned status %d: %s", resp.StatusCode, string(bodyBytes))
}

// Parse and display success output
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("json.Decode: %w", err)
}

fmt.Fprintln(w, "\n--- SUCCESS! ---")
fmt.Fprintln(w, "Successfully authenticated and retrieved bucket data:")
prettyJSON, _ := json.MarshalIndent(result, "", " ")
fmt.Fprintln(w, string(prettyJSON))

return nil
}

// [END auth_custom_credential_supplier_aws]

func main() {
gcpAudience := os.Getenv("GCP_WORKLOAD_AUDIENCE")
// Service account impersonation is optional
saImpersonationURL := os.Getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL")
gcsBucketName := os.Getenv("GCS_BUCKET_NAME")

if gcpAudience == "" || gcsBucketName == "" {
fmt.Fprintln(os.Stderr, "Missing required environment variables: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME")
os.Exit(1)
}

// Pass os.Stdout as the writer to print to console
if err := authenticateWithAwsCredentials(os.Stdout, gcsBucketName, gcpAudience, saImpersonationURL); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
105 changes: 105 additions & 0 deletions auth/custom_credential_supplier/aws/aws_custom_credential_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"context"
"testing"

"cloud.google.com/go/auth/credentials/externalaccount"
)

// TestCustomAwsSupplier_AwsRegion verifies that the supplier correctly
// resolves the Region from environment variables, respecting precedence.
func TestCustomAwsSupplier_AwsRegion(t *testing.T) {
ctx := context.Background()
supplier := &customAwsSupplier{}
opts := &externalaccount.RequestOptions{}

tests := []struct {
name string
env map[string]string
wantRegion string
}{
{
name: "AWS_REGION is set (Highest Priority)",
env: map[string]string{
"AWS_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-east-1",
},
wantRegion: "us-west-1",
},
{
name: "Only AWS_DEFAULT_REGION is set (Fallback)",
env: map[string]string{
"AWS_REGION": "",
"AWS_DEFAULT_REGION": "eu-central-1",
},
wantRegion: "eu-central-1",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Set environment variables for this test case
for k, v := range tc.env {
t.Setenv(k, v)
}

got, err := supplier.AwsRegion(ctx, opts)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

if got != tc.wantRegion {
t.Errorf("AwsRegion() = %v, want %v", got, tc.wantRegion)
}
})
}
}

// TestCustomAwsSupplier_AwsSecurityCredentials verifies that the supplier
// correctly extracts credentials from the AWS environment variables.
func TestCustomAwsSupplier_AwsSecurityCredentials(t *testing.T) {
ctx := context.Background()
supplier := &customAwsSupplier{}
opts := &externalaccount.RequestOptions{}

// Mock AWS Credentials via Environment Variables
// The AWS SDK v2 automatically picks these up.
expectedID := "AKIA_TEST_ACCESS_KEY"
expectedSecret := "TEST_SECRET_KEY"
expectedToken := "TEST_SESSION_TOKEN"

t.Setenv("AWS_ACCESS_KEY_ID", expectedID)
t.Setenv("AWS_SECRET_ACCESS_KEY", expectedSecret)
t.Setenv("AWS_SESSION_TOKEN", expectedToken)
t.Setenv("AWS_REGION", "us-east-1") // Required for SDK config to load successfully

creds, err := supplier.AwsSecurityCredentials(ctx, opts)
if err != nil {
t.Fatalf("AwsSecurityCredentials failed: %v", err)
}

if creds.AccessKeyID != expectedID {
t.Errorf("AccessKeyID = %v, want %v", creds.AccessKeyID, expectedID)
}
if creds.SecretAccessKey != expectedSecret {
t.Errorf("SecretAccessKey = %v, want %v", creds.SecretAccessKey, expectedSecret)
}
if creds.SessionToken != expectedToken {
t.Errorf("SessionToken = %v, want %v", creds.SessionToken, expectedToken)
}
}
Loading