iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔐

Testing API Gateway Authentication with JWTs Issued by AWS IAM | Outbound Identity Federation in Practice

に公開

Introduction

In November 2025, AWS announced a new feature called IAM Outbound Identity Federation. This feature allows AWS IAM principals (Roles or Users) to obtain short-lived JWTs and use them for authentication with external services.

In this article, I tried authenticating with API Gateway HTTP API's JWT authorizer using a JWT issued by this feature.

What is IAM Outbound Identity Federation?

IAM Outbound Identity Federation is a feature where AWS IAM principals (Roles or Users) obtain short-lived JWT tokens and use those tokens to authenticate to external services.

https://aws.amazon.com/about-aws/whats-new/2025/11/aws-iam-identity-federation-external-services-jwts/

Main features:

  • Obtain JWTs via the AWS STS GetWebIdentityToken API
  • Tokens include information such as the originating AWS account and principal details
  • External services can verify tokens using a JWKS (JSON Web Key Set) endpoint
  • Token duration is between 60 to 3600 seconds (1 minute to 1 hour), with a default of 300 seconds (5 minutes)

https://aws.amazon.com/blogs/aws/simplify-access-to-external-services-using-aws-iam-outbound-identity-federation/

Architecture

The configuration of the system to be built this time is as follows:

Components:

  • API Gateway (HTTP API): Verifies tokens using a JWT authorizer
  • Lambda Function: Backend handler
  • IAM Issuer URL: Provides the JWKS endpoint for token verification

Implementation Steps

Prerequisites

  • AWS CLI is installed and properly configured
  • AWS CDK is installed
  • Node.js/TypeScript environment

Obtaining the Issuer URL

First, enable IAM Outbound Identity Federation and obtain the Issuer URL.

  1. Open "Account settings" in the IAM console
  2. Enable the feature in the "Outbound Identity Federation" section
  3. Note the displayed Issuer URL (format: https://xxxx.tokens.sts.global.api.aws)

Deploying with CDK

Deploy the API Gateway and Lambda function using CDK.

lib/gekko-001-stack.ts
import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { HttpApi } from "aws-cdk-lib/aws-apigatewayv2";
import { HttpJwtAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { HttpMethod } from "aws-cdk-lib/aws-events";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";

export class Gekko001Stack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const handler = new NodejsFunction(this, 'HelloWorldHandler', {
      runtime: Runtime.NODEJS_LATEST,
      entry: `${__dirname}/hello-handler.ts`,
      handler: 'handler',
      logGroup: new LogGroup(this, 'HelloWorldLogGroup', {
        logGroupName: '/gekko-001/hello-world',
        removalPolicy: RemovalPolicy.DESTROY,
        retention: RetentionDays.ONE_DAY,
      }),
    });

    const apiGw = new HttpApi(this, 'ApiGw');
    const integration = new HttpLambdaIntegration('HelloWorldIntegration', handler);

    // JWT Authorizer settings
    apiGw.addRoutes({
      methods: [HttpMethod.GET],
      path: '/jwt/hello',
      integration: integration,
      authorizer: new HttpJwtAuthorizer('IamJwtAuthorizer',
        'https://YOUR-ISSUER-ID.tokens.sts.global.api.aws', {
        jwtAudience: [apiGw.httpApiName!],
      }),
    });

    new CfnOutput(this, 'ApiGwUrl', {
      value: apiGw.apiEndpoint,
    });
    new CfnOutput(this, 'ApiGwName', {
      value: apiGw.httpApiName!,
    });
  }
}

Lambda function handler code:

lib/hello-handler.ts
import { APIGatewayProxyEventV2, Context } from 'aws-lambda';

export const handler = async (event: APIGatewayProxyEventV2, context: Context) => {
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'Hello, World!',
            event: event,
            context: context
        }),
    };
}

Key points:

  • Specify the IAM Issuer URL in HttpJwtAuthorizer
  • Set the API Gateway name in jwtAudience. This will also be used as the value specified when obtaining the token from IAM.

Deployment:

cdk deploy

Obtaining the token and accessing the API

Here is a script to obtain a token and access the API Gateway:

scripts/jwt-get.sh
#!/bin/bash

if [ -z "$1" ]; then
  echo "Usage: $0 <URL>" >&2
  exit 1
fi

TARGET_URL="$1"

# Obtain JWT token
JWT_TOKEN=$(aws sts get-web-identity-token \
  --audience ApiGw \
  --signing-algorithm RS256 \
  --query 'WebIdentityToken' \
  --output text)

# Access API Gateway
curl -s -X GET "$TARGET_URL" \
  -H "Authorization: Bearer $JWT_TOKEN"

Execution:

./scripts/jwt-get.sh https://YOUR-API-ID.execute-api.REGION.amazonaws.com/jwt/hello

Options used in the get-web-identity-token command within the script:

  • --audience ApiGw: The value set in the JWT's aud claim (must match the jwtAudience setting of the authorizer).
  • --signing-algorithm RS256: The signing algorithm (can be selected from RS256=RSA or ES384=ECDSA).
  • The token's validity period defaults to 300 seconds (5 minutes) (can be specified between 60 to 3600 seconds with --duration-seconds).

Operation Check

If the access is successful, you will receive a response like this:

scripts/jwt-get-response.json
{
  "message": "Hello, World!",
  "event": {
    "version": "2.0",
    "routeKey": "GET /jwt/hello",
    "requestContext": {
      "authorizer": {
        "jwt": {
          "claims": {
            "aud": "ApiGw",
            "sub": "arn:aws:iam::ACCOUNT_ID:role/...",
            "iss": "https://xxxx.tokens.sts.global.api.aws",
            "exp": "1763824092",
            "iat": "1763823192"
          }
        }
      }
    }
  }
}

Within the Lambda function, you can access the token's claim information (issuer, subject, expiration, etc.) from event.requestContext.authorizer.jwt.claims.

Access Control via IAM

Required Permissions

To issue a token, the IAM principal needs the sts:GetWebIdentityToken permission.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:GetWebIdentityToken",
      "Resource": "*"
    }
  ]
}

Restrictions Using Condition Keys

You can further restrict token issuance using the following condition keys:

Condition Key Description Example
sts:IdentityTokenAudience Restricts the audiences that can be issued "StringEquals": {"sts:IdentityTokenAudience": "ApiGw"}
sts:DurationSeconds Restricts the token validity period "NumericLessThanEquals": {"sts:DurationSeconds": "3600"}
sts:SigningAlgorithm Restricts the signing algorithm "StringEquals": {"sts:SigningAlgorithm": "RS256"}

https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssecuritytokenservice.html

Practical Policy Example

An example that allows only specific audiences and restricts the validity period:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:GetWebIdentityToken",
      "Resource": "*",
      "Condition": {
        "ForAnyValue:StringEquals": {
          'sts:IdentityTokenAudience': ["ApiGw", "ApiGwProd"]
        }
        "NumericLessThanEquals": {
          "sts:DurationSeconds": "1800"
        }
      }
    }
  ]
}

With this policy:

  • Only tokens with the audience ApiGw or ApiGwProd can be issued.
  • The token validity period is limited to a maximum of 30 minutes (1800 seconds).

Comparison with Other Solutions

We compare three authentication methods for API Gateway: IAM Outbound Federation, IAM authorizer, and Cognito.

Method Client Implementation Portability Complexity Typical Use Case
IAM Outbound Federation Obtain token with aws sts get-web-identity-token → add to HTTP header High (Standard JWT) Low M2M communication, testing environments, prototyping
IAM Authorizer SigV4 signature for the entire request (specialized tools required) Low (AWS specific) High Communication between internal AWS services
Cognito User Pools OAuth 2.0 / OIDC flow Medium (Industry standard) Medium End-user authentication, full-scale apps

Detailed Comparison

vs IAM Authorizer

You might wonder, "If I'm controlling token issuance with IAM anyway, isn't using an IAM authorizer on the API Gateway side the same thing?"

Key difference: Complexity of client-side implementation

Item IAM Outbound Federation (JWT) IAM Authorizer
Authentication Method JWT Token SigV4 Signature
Client Implementation Add Authorization: Bearer <token> header Sign the entire request (headers, path, query, body)
Required Tools AWS CLI/SDK only Specialized tools like awscurl or complex implementation
Portability High (Standard HTTP/JWT) Low (AWS specific)
External Service Integration Easy Difficult

This difference becomes especially important when integrating with external services.

vs Cognito User Pools

One might wonder, "Why not just use Cognito?" While Cognito is indeed an excellent choice, you can choose between them based on the use case.

Item IAM Outbound Identity Federation Cognito User Pools
Setup
Required Resources None (IAM settings only) User Pool, App Client, Resource Server, etc.
Initial Setup Effort Minimal Moderate
Authentication Features
Access Control Audience-based (Simple) Scope-based (Flexible)
Custom Claims Can be added via request tags Can be added flexibly
User Management None Sign-up, password reset, etc.
Auth Flows Token acquisition only OAuth 2.0, SAML, Social login, etc.
Client Implementation
Secret Management Not required Client ID, secrets, etc.
Implementation Complexity Simple Moderate

Recommended Use Cases:

  • IAM Method: M2M communication within AWS, prototyping, simple authentication requirements, testing environments.
  • Cognito: End-user authentication, complex permission management, full-scale applications, rich authentication flows.

⚠️ Notes on Production Use

Deviation from Design Intent

Important: This feature is originally designed for AWS workloads to access external services. Using it with API Gateway as shown here is a use case that differs from the intended design.

The official documentation also assumes use with external services (third-party clouds, SaaS, on-premises applications).

When considering use in a production environment, the following use cases are suitable:

Suitable:

  • Temporary authentication between microservices within AWS (prototyping)
  • Simple authentication in development and testing environments
  • Outbound communication to external services (original purpose)

Not Suitable:

  • Full-scale end-user applications
  • Systems requiring complex permission management
  • Cases where scope-based access control is required

Security Considerations

Points to note when using in a production environment:

  1. Token Validity Period

    • Adjust the default 5 minutes according to the application.
    • Set to the minimum necessary duration.
  2. Audience Design

    • Use a different audience for each API or service.
    • Avoid using wildcards or generic values.
  3. IAM Policy Hardening

    • Restrict token issuance using condition keys.
    • Apply the principle of least privilege.
  4. Auditing and Monitoring

    • Record token issuance in CloudTrail.
    • Detect abnormal token issuance patterns.

Summary

In this article, we explored how to use JWTs issued by AWS IAM Outbound Identity Federation to authenticate with an API Gateway JWT authorizer.

Key Findings:

  • JWTs issued by IAM can be verified using the API Gateway JWT authorizer.
  • The implementation is simple and can be treated as a standard JWT authentication flow.
  • Token issuance can be finely controlled using IAM policies.

Applicable Scenes:

  • Lightweight authentication solutions for M2M communication within AWS.
  • Use in prototyping or development environments.
  • Simple use cases where the extensive features of Cognito are not required.

Points to Note:

  • This feature is originally intended for accessing external services, so using it in this way deviates from the design intent.
  • Use cases in production environments should be carefully considered.
  • Consider using Cognito if scope-based permission management is required.

While the original purpose of this feature is to enable access to external services, I felt there is potential for applying it to access control between services running on AWS. I look forward to exploring integration patterns with other AWS services in the future.

Discussion