# Workload Identity Federation with AWS

Authenticate to the Stripe API from AWS without a long-lived API key.

> This feature is in private preview. To join the preview, [request access](https://docs.stripe.com/workload-identity-federation.md#request-access).

Your workload’s IAM role is the identity that Stripe sees in the token exchange. After you enable outbound identity federation for your AWS account, IAM roles with the WIF permission policy can get AWS-signed identity tokens for Stripe.

## Before you begin

- Create a Stripe [organization](https://docs.stripe.com/get-started/account/orgs.md) with [private preview access](https://docs.stripe.com/workload-identity-federation.md#request-access) to WIF.
- Create an AWS account with permissions to create IAM roles and change IAM account settings.
- To set up with the CLI, install a recent version of the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). To set up with Terraform, install [Terraform](https://developer.hashicorp.com/terraform/install) 1.5 or later.

## Configure an IAM role

#### AWS Management Console

### Enable outbound identity federation 

1. Open the [AWS Management Console](https://console.aws.amazon.com) and click **IAM**.
2. In the left menu, under Access management, select **Account settings**.
3. Find the **Outbound identity federation** section and select **Enable**.
4. Copy the **token issuer URL** that you see (**https://<uuid>.tokens.sts.global.api.aws**). You’ll need this in Stripe.

### Add the WIF policy to your workload’s IAM role 

Add the WIF permission policy to the role your workload already uses, then register that role’s ARN as the subject in Stripe.

If your workload already has a role (most production workloads do), find it:

- **Lambda**: Open the function, go to **Configuration** > **Permissions**, and click the execution role name.
- **EC2**: Select the instance, go to **Security**, and click the IAM role name.
- **ECS**: open the task definition and find the task role (not the task execution role, which ECS uses to pull images and write logs).

If your workload doesn’t have a role yet, or you’re [testing locally](https://docs.stripe.com/workload-identity-federation/aws.md#test-locally), create one:

1. Go to **IAM** > **Roles** > **Create role**.
2. For **Trusted entity type**, choose **AWS service** for a workload or **AWS account** > **This account** for local testing.
3. Skip the “Add permissions” step and click **Next**.
4. Name the role (for example `stripe-caller`) and choose **Create role**.

Add the WIF policy to the role:

1. Open the role in IAM, go to the **Permissions** tab, and click **Add permissions** > **Create inline policy**.
2. Switch to the JSON editor and paste:

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "sts:GetWebIdentityToken",
    "Resource": "*",
    "Condition": {
      "ForAllValues:StringEquals": { "sts:IdentityTokenAudience": "https://access.stripe.com/wif" }
    }
  }]
}
```

1. Name the policy (for example `stripe-wif`) and save it.
2. Copy the role’s **ARN** from the top of the role page. This is the service principal’s subject in Stripe.

#### CLI

### Enable outbound identity federation 

Enable outbound identity federation for your AWS account and save its token issuer URL. If you already enabled federation, the first command returns a `FeatureEnabledException` error, and you can continue.

```bash
aws iam enable-outbound-web-identity-federation
ISSUER_URL=$(aws iam get-outbound-web-identity-federation-info --query IssuerIdentifier --output text)
```

### Create the IAM role 

If your workload already has a role (most production workloads do), set `ROLE_NAME` to that role’s name, skip `create-role`, and [add the WIF policy](https://docs.stripe.com/workload-identity-federation/aws.md#cli-add-wif-policy).

Otherwise, create a role. The following trust policy lets principals in your AWS account assume the role, which you need to [test locally](https://docs.stripe.com/workload-identity-federation/aws.md#test-locally). For a workload, replace the `Principal` with the AWS service that runs it, such as `{ "Service": "lambda.amazonaws.com" }`.

```bash
ROLE_NAME=stripe-caller
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

cat > trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
    "Action": "sts:AssumeRole"
  }]
}
EOF

aws iam create-role --role-name "$ROLE_NAME" --assume-role-policy-document file://trust-policy.json
```

### Add the WIF policy to the role 

Add the WIF policy, then print the issuer URL and subject that you need in Stripe:

```bash
cat > stripe-wif-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "sts:GetWebIdentityToken",
    "Resource": "*",
    "Condition": {
      "ForAllValues:StringEquals": { "sts:IdentityTokenAudience": "https://access.stripe.com/wif" }
    }
  }]
}
EOF

aws iam put-role-policy --role-name "$ROLE_NAME" --policy-name stripe-wif \
  --policy-document file://stripe-wif-policy.json

echo "Issuer URL: $ISSUER_URL"
echo "Subject:    $(aws iam get-role --role-name "$ROLE_NAME" --query Role.Arn --output text)"
```

#### Terraform

Save the following configuration as `main.tf`. It enables outbound identity federation, creates the IAM role with the WIF policy, and outputs the issuer URL and subject that you need in Stripe.

> Outbound identity federation is an account-wide setting. If this configuration enables it, running `terraform destroy` disables it for your entire AWS account. If you already enabled federation, set `manage_outbound_federation` to `false` so that Terraform reads the existing issuer URL instead of managing the setting.

```hcl
terraform {
  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.36"
    }
  }
}

variable "region" {
  description = "The AWS region for the provider. IAM is global, so any region works."
  type        = string
  default     = "us-east-1"
}

variable "role_name" {
  description = "The name of the IAM role your workload assumes."
  type        = string
  default     = "stripe-caller"
}

variable "workload_service" {
  description = "Optional. The AWS service that runs your workload and assumes the role, such as lambda.amazonaws.com, ec2.amazonaws.com, or ecs-tasks.amazonaws.com."
  type        = string
  default     = null
}

variable "allow_local_testing" {
  description = "Allow principals in this AWS account to assume the role, for local testing with the AWS CLI. Leave false in production."
  type        = bool
  default     = false
}

variable "manage_outbound_federation" {
  description = "Enable outbound identity federation for the account. Set to false if it's already enabled."
  type        = bool
  default     = true
}

provider "aws" {
  region = var.region
}

data "aws_caller_identity" "current" {}

# Outbound identity federation lets IAM roles in this account get AWS-signed
# identity tokens. It's an account-wide setting.

resource "aws_iam_outbound_web_identity_federation" "this" {
  count = var.manage_outbound_federation ? 1 : 0
}

data "aws_iam_outbound_web_identity_federation" "existing" {
  count = var.manage_outbound_federation ? 0 : 1
}

locals {
  issuer_url = var.manage_outbound_federation ? aws_iam_outbound_web_identity_federation.this[0].issuer_identifier : data.aws_iam_outbound_web_identity_federation.existing[0].issuer_identifier
}

# The role your workload assumes.

data "aws_iam_policy_document" "trust" {
  dynamic "statement" {
    for_each = var.workload_service == null ? [] : [var.workload_service]
    content {
      actions = ["sts:AssumeRole"]
      principals {
        type        = "Service"
        identifiers = [statement.value]
      }
    }
  }

  dynamic "statement" {
    for_each = var.allow_local_testing ? [data.aws_caller_identity.current.account_id] : []
    content {
      actions = ["sts:AssumeRole"]
      principals {
        type        = "AWS"
        identifiers = ["arn:aws:iam::${statement.value}:root"]
      }
    }
  }
}

resource "aws_iam_role" "stripe_caller" {
  name               = var.role_name
  description        = "Authenticates to the Stripe API with Workload Identity Federation."
  assume_role_policy = data.aws_iam_policy_document.trust.json

  lifecycle {
    precondition {
      condition     = var.workload_service != null || var.allow_local_testing
      error_message = "Set workload_service, allow_local_testing, or both, so something can assume the role."
    }
  }
}

# Lets the role get identity tokens for Stripe's audience only.

data "aws_iam_policy_document" "stripe_wif" {
  statement {
    actions   = ["sts:GetWebIdentityToken"]
    resources = ["*"]

    condition {
      test     = "ForAllValues:StringEquals"
      variable = "sts:IdentityTokenAudience"
      values   = ["https://access.stripe.com/wif"]
    }
  }
}

resource "aws_iam_role_policy" "stripe_wif" {
  name   = "stripe-wif"
  role   = aws_iam_role.stripe_caller.id
  policy = data.aws_iam_policy_document.stripe_wif.json
}

output "stripe_issuer_url" {
  description = "The issuer URL for your service principal in Stripe."
  value       = local.issuer_url
}

output "stripe_subject" {
  description = "The subject for your service principal in Stripe."
  value       = aws_iam_role.stripe_caller.arn
}
```

Initialize Terraform and apply the configuration. To [test locally](https://docs.stripe.com/workload-identity-federation/aws.md#test-locally), allow principals in your AWS account to assume the role:

```bash
terraform init
terraform apply -var="allow_local_testing=true"
```

For a workload, pass the AWS service that runs it instead, and don’t set `allow_local_testing` in production:

```bash
terraform apply -var="workload_service=lambda.amazonaws.com"
```

Terraform prints the `stripe_issuer_url` and `stripe_subject` outputs. You need them when you create a service principal in Stripe.

## Create a service principal in Stripe

1. In the Dashboard, go to **Organization** > **Developers** > **API authentication** > [Service principals](https://dashboard.stripe.com/org/service-principals).
2. Click **Create service principal**.
3. Select an existing workload identity provider, or add a new one.
   - Enter the `Issuer URL` from your workload identity provider.
4. Paste the subject of your service from your cloud provider. You can add multiple subjects if your service principal is acting on behalf of multiple workloads.
5. Assign permissions to your service principal. Select between common permission templates, or assign custom permissions to specific API resources.
6. Name your service principal and review its details and assigned permissions.
7. After you create the service principal, copy its client ID `oacli_...`. Use this client ID when you exchange tokens in the next step.

For AWS, use the following values:

- **Issuer URL**: Your AWS account’s token issuer URL, such as `https://<uuid>.tokens.sts.global.api.aws`
- **Subject**: The IAM role ARN your workload assumes, such as `arn:aws:iam::123456789012:role/stripe-caller`

## Authenticate requests to the Stripe API

> During private preview, Workload Identity Federation with AWS is only supported using the Node.js SDK.

The Stripe SDKs automatically handle the creation and exchange of short-lived access tokens for your workload to make calls to the Stripe API from your cloud provider. Make sure to include the Workload Identity Federation SDK dependency `@stripe/workload-identity` in addition to the main `stripe` SDK. Install the private-preview builds of both packages:

```bash
npm install stripe@private-preview @stripe/workload-identity@private-preview
```

Initialize the Stripe client and pass in the service principal’s client ID along with the `stripeContext` to specify the account context for the request.

> #### Replace the placeholder IDs
> 
> Before you run this code, replace both placeholders with your own values:
> 
> - `oacli_...`: Your service principal’s client ID, which you copied after you [created the service principal](https://docs.stripe.com/workload-identity-federation/aws.md#create-service-principal).
> - `acct_...`: The ID of the Stripe account in your organization that the request acts on. See [Specify request context](https://docs.stripe.com/context.md).

```node
import Stripe from 'stripe';
import {awsWorkloadIdentity} from '@stripe/workload-identity';

const stripe = Stripe.forWorkloadIdentity(
  'oacli_...', // Replace with your service principal's client ID
  awsWorkloadIdentity(),
  {
    stripeContext: 'acct_...', // Replace with your Stripe account ID
  }
);

const customer = await stripe.customers.create(
  {
    name: 'Jenny Rosen',
    email: 'jenny@example.com',
  }
);
console.log(customer.id);
```

## Test your workload locally

Test your integration to validate that the workload in your cloud provider can properly authenticate API requests in Stripe. Make sure your role’s trust policy [allows local testing](https://docs.stripe.com/workload-identity-federation/aws.md#configure-iam-role).

If you’re testing locally, use the AWS CLI to assume the role, then call the Stripe API from a local script. First, check your role:

```bash
aws sts get-caller-identity
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
```

Next, create an AWS CLI profile so you can assume the role locally. Run these commands in the same terminal session as the previous step so that `ACCOUNT_ID` is set:

```bash
aws configure set role_arn "arn:aws:iam::${ACCOUNT_ID}:role/stripe-caller" --profile stripe-caller
aws configure set source_profile default --profile stripe-caller
aws configure set region us-east-1 --profile stripe-caller
```

Check that it works:

```bash
aws sts get-caller-identity --profile stripe-caller
```

The ARN in the output looks like the following: `arn:aws:sts::<ACCOUNT_ID>:assumed-role/stripe-caller/...`. That shows you’re acting as the role.

As a last step, write the test script, set up the project, and run the script with the stripe-caller profile so it authenticates as the role and not with your default credentials.

> #### Replace the placeholder IDs
> 
> In the first two lines of the following commands, replace both placeholders with your own values:
> 
> - `oacli_...`: Your service principal’s client ID, which you copied after you [created the service principal](https://docs.stripe.com/workload-identity-federation/aws.md#create-service-principal).
> - `acct_...`: The ID of the Stripe account in your organization that the request acts on. See [Specify request context](https://docs.stripe.com/context.md).
> 
> The script stops with an error if you leave either placeholder unchanged.

```bash
export STRIPE_CLIENT_ID=oacli_...  # Replace with your service principal's client ID
export STRIPE_ACCOUNT=acct_...     # Replace with your Stripe account ID

cat > stripe_wif_test.mjs <<'EOF'
import Stripe from 'stripe';
import {awsWorkloadIdentity} from '@stripe/workload-identity';

// Stops the script if a placeholder ID wasn't replaced.
function requiredEnv(name) {
  const value = process.env[name] ?? '';
  if (!value || value.endsWith('...')) {
    throw new Error(`Set ${name} to your own value before running this script.`);
  }
  return value;
}

const stripe = Stripe.forWorkloadIdentity(
  requiredEnv('STRIPE_CLIENT_ID'),
  awsWorkloadIdentity(),
  {
    stripeContext: requiredEnv('STRIPE_ACCOUNT'),
  }
);

const customer = await stripe.customers.create(
  {
    name: 'Jenny Rosen',
    email: 'jenny@example.com',
  }
);
console.log(customer.id);
EOF

npm init -y
npm pkg set type=module
npm install stripe@private-preview @stripe/workload-identity@private-preview
AWS_PROFILE=stripe-caller node stripe_wif_test.mjs
```

The script prints the ID of the new customer, such as `cus_...`.

## See also

- [Workload Identity Federation](https://docs.stripe.com/workload-identity-federation.md)
- [Service principals](https://docs.stripe.com/service-principals.md)
