Skip to content
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ testing/kokoro/test-env.sh
.DS_Store
go.work.sum
sponge_log.xml

# Secrets files
secrets.json
custom-credentials-aws-secrets.json
custom-credentials-okta-secrets.json
29 changes: 29 additions & 0 deletions auth/custom_credential_supplier/aws/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# --- Stage 1: Build the application ---
FROM golang:1.21 AS builder

WORKDIR /app

# Download dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build a static binary
# CGO_ENABLED=0 ensures the binary is statically linked and works on Alpine
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

# --- Stage 2: Run the application ---
FROM alpine:3.18

WORKDIR /app

# Install CA certificates for HTTPS requests to Google APIs
RUN apk add --no-cache ca-certificates

# Copy the binary from the builder stage
COPY --from=builder /app/app .

# Run the binary
CMD ["./app"]
113 changes: 113 additions & 0 deletions auth/custom_credential_supplier/aws/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Running the Custom AWS Credential Supplier Sample (Go)

This sample demonstrates how to use a custom AWS security credential supplier to authenticate with Google Cloud using AWS as an external identity provider. It uses the **AWS SDK for Go v2** to fetch credentials from sources like Amazon Elastic Kubernetes Service (EKS) with IAM Roles for Service Accounts (IRSA), Elastic Container Service (ECS), or Fargate.

## Prerequisites

* An AWS account.
* A Google Cloud project with the IAM API enabled.
* A Google Cloud Storage bucket.
* **Go 1.21** or later installed.

If you want to use AWS security credentials that cannot be retrieved using methods supported natively by the Google Auth library, a custom `AwsSecurityCredentialsProvider` implementation may be specified.

## Running Locally

For local development, you can provide credentials and configuration in a JSON file.

### 1. Install Dependencies

Initialize the module and download required packages:

```bash
go mod tidy
```

### 2. Configure Credentials

1. Copy the example secrets file to a new file named `custom-credentials-aws-secrets.json` in the project root:
```bash
cp custom-credentials-aws-secrets.json.example custom-credentials-aws-secrets.json
```
2. Open `custom-credentials-aws-secrets.json` and fill in the required values for your AWS and Google Cloud configuration. Do not check your `custom-credentials-aws-secrets.json` file into version control.

**Note:** Do not check your secrets file into version control.

### 3. Run the Application

Execute the Go program:

```bash
go run .
```

The application will detect the `custom-credentials-aws-secrets.json` file, use the AWS SDK to resolve credentials, exchange them for a Google Cloud token, and retrieve metadata for your GCS bucket using the Google Cloud Storage Client Library.

## 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 without hardcoding long-lived credentials.

Run the following command to create the IAM role and bind it to a Kubernetes Service Account:

```bash
eksctl create iamserviceaccount \
--name your-k8s-service-account \
--namespace default \
--cluster your-cluster-name \
--region your-aws-region \
--role-name your-role-name \
--attach-policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--approve
```

> **Note**: The `--attach-policy-arn` flag is used here to demonstrate attaching permissions. Update this with the specific AWS policy ARN your application requires.

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 Google Cloud to Trust the AWS Role

You need to configure your Google Cloud project to trust the AWS IAM role you created.

1. **Create a Workload Identity Pool and Provider** that trusts your AWS account.
2. **Bind the AWS Role to a Google Cloud Service Account** (or grant permissions directly to the federated identity).

For detailed steps, see [Configuring Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds).

### 4. Containerize and Package the Application

Create a `Dockerfile` for the Go application. See the [`Dockerfile`](Dockerfile) file for an example.

Build and push the image to your registry (e.g., Amazon ECR):

```bash
docker build -t your-container-image:latest .
docker push your-container-image:latest
```

### 5. Deploy to EKS

Create a Kubernetes deployment manifest to deploy your application to the EKS cluster. See the [`pod.yaml`](pod.yaml) file for an example.

**Note:** The provided [`pod.yaml`](pod.yaml) is an example and may need to be modified for your specific needs.

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
```
153 changes: 153 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,153 @@
// 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"
"fmt"
"io"
"net/http"

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

// customAwsSupplier implements externalaccount.AwsSecurityCredentialsProvider
type customAwsSupplier struct{}

// AwsRegion resolves the region from the AWS SDK default config.
func (s *customAwsSupplier) AwsRegion(ctx context.Context, _ *externalaccount.RequestOptions) (string, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", err
}
if cfg.Region == "" {
return "", fmt.Errorf("AWS_REGION not set")
}
return cfg.Region, nil
}

// AwsSecurityCredentials retrieves credentials via the AWS SDK.
func (s *customAwsSupplier) AwsSecurityCredentials(ctx context.Context, _ *externalaccount.RequestOptions) (*externalaccount.AwsSecurityCredentials, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, err
}
c, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, err
}
return &externalaccount.AwsSecurityCredentials{
AccessKeyID: c.AccessKeyID, SecretAccessKey: c.SecretAccessKey, SessionToken: c.SessionToken,
}, nil
}

// authenticateWithAwsCredentials authenticates with Google Cloud using the custom supplier.
func authenticateWithAwsCredentials(w io.Writer, bucketName, audience, impersonationURL string) error {
ctx := context.Background()

// Initialize credentials with the custom supplier
creds, err := externalaccount.NewCredentials(&externalaccount.Options{
Audience: audience,
SubjectTokenType: "urn:ietf:params:aws:token-type:aws4_request",
ServiceAccountImpersonationURL: impersonationURL,
AwsSecurityCredentialsProvider: &customAwsSupplier{},
Scopes: []string{"https://www.googleapis.com/auth/devstorage.read_only"},
})
if err != nil {
return fmt.Errorf("NewCredentials: %w", err)
}

// Fetch the OAuth2 token
token, err := creds.Token(ctx)
if err != nil {
return fmt.Errorf("creds.Token: %w", err)
}

// Verify access by making a raw HTTP request to the GCS API
url := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s", bucketName)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token.Value)

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

if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("request failed (%d): %s", resp.StatusCode, string(b))
}

fmt.Fprintf(w, "Success! Authenticated and accessed bucket: %s\n", bucketName)
return nil
}

// [END auth_custom_credential_supplier_aws]

// Note: The code below handles local configuration loading and is not part of the core sample.

func main() {
loadConfigFromFile()

audience := os.Getenv("GCP_WORKLOAD_AUDIENCE")
bucket := os.Getenv("GCS_BUCKET_NAME")
impersonationURL := os.Getenv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL")

if audience == "" || bucket == "" {
fmt.Fprintln(os.Stderr, "Missing required configuration: GCP_WORKLOAD_AUDIENCE, GCS_BUCKET_NAME")
os.Exit(1)
}

if err := authenticateWithAwsCredentials(os.Stdout, bucket, audience, impersonationURL); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

func loadConfigFromFile() {
file, err := os.Open("custom-credentials-aws-secrets.json")
if err != nil {
return // File missing, rely on env vars
}
defer file.Close()

var s struct {
Audience string `json:"gcp_workload_audience"`
Bucket string `json:"gcs_bucket_name"`
ImpURL string `json:"gcp_service_account_impersonation_url"`
AwsID string `json:"aws_access_key_id"`
AwsSecret string `json:"aws_secret_access_key"`
AwsRegion string `json:"aws_region"`
}
if json.NewDecoder(file).Decode(&s) == nil {
setEnv("AWS_ACCESS_KEY_ID", s.AwsID)
setEnv("AWS_SECRET_ACCESS_KEY", s.AwsSecret)
setEnv("AWS_REGION", s.AwsRegion)
setEnv("GCP_WORKLOAD_AUDIENCE", s.Audience)
setEnv("GCS_BUCKET_NAME", s.Bucket)
setEnv("GCP_SERVICE_ACCOUNT_IMPERSONATION_URL", s.ImpURL)
}
}

func setEnv(k, v string) {
if v != "" {
os.Setenv(k, v)
}
}
Loading
Loading