Authenticating to MongoDB Atlas with AWS Outbound Identity Federation

I have been writing several posts on authentication and authorization topics, using PEP and PDP pattern. PEP and PDP for Secure Authorization with Cognito, then extended that with Amazon Verified Permissions, and later added ABAC on top. All of that was about user-to-service authorization. Meaning a human logs in, gets a token, and the backend decides what they can do.
But what about service-to-service? Machine-to-machine? Your Lambda needs to talk to a database or call an external API, and there's no human in the loop. You would still need credentials.
The Usual Way to Handle M2M Tokens
One of most common approaches I see is using Cognito User Pools, Auth0, or similar with the OAuth 2.0 client credentials flow. You create a resource server, register an app client with a client ID and secret, and your Lambda exchanges those for an access token. It works well, and if you're already running Cognito for your user authentication it feels natural to extend it for M2M too. What should be remembered is that M2M tokens from these services quickly can become expensive.
One other classic approach is just storing credentials in Secrets Manager. Database passwords, API keys, whatever you need. Enable rotation and move on. It works, but it's one more thing to manage, one more thing that can break, and one more secret that can leak.
Outbound Identity Federation
Just before re:Invent 2025 there was a very interesting release, AWS IAM Outbound Identity Federation.
We have been using identity federation in AWS for a long time. Google, Okta, Entra, or other issues a token, AWS trusts it, and you get access to AWS resources. That's inbound federation. AWS is the one doing the trusting.
Outbound Identity Federation is the reverse. Now AWS is the one issuing tokens. Your Lambda (or EC2, or ECS task) call STS to exchange an IAM role for a signed JWT token that basically says "hey, I'm this IAM role, and here's a cryptographic proof." Then you hand that token to whatever external or internal service you're talking to, and they can verify it using standard flows.
That mean, no client secrets or stored passwords. The issued token is short-lived, tied to the IAM role, and signed by AWS. The receiving service just needs to trust the issuer.
What's in the Token?
When we call sts:GetWebIdentityToken, we'll get back a standard JWT token, with a few claims that matter:
iss - The issuer. This is the Token Issuer URL we'll get this when we enable the feature, it looks like this "https://a1810f8a-5a75-4e21-b1cd-a6b09f1836cb.tokens.sts.global.api.aws". It's unique to our AWS account and we can't modify it.sub - The subject. This is the IAM Role ARN. It's the identity.aud - The audience. This is set bu us, and it can be anything, "my-api, "atlas-demo", whatever. Both sides just need to agree on the value.exp - Expiration. Short-lived is the whole point.
Architecture overview
In this post we will build two different solutions that use the Outbound Identity Federation, just to show how to set it up for an internal service, hosted on Lambda behind API Gateway, and an external database, in this case MongoDB Atlas.
In our internal use case we have a worker Lambda function that will call STS to get a signed outbound JWT token. The functions call an API in API Gateway where we have a Lambda Authorizer that use the public keys from STS to validate the JWT.

In the MongoDB case we pretty much have the same. We establish trust between MongoDB and STS by making some configuration on the MongoDB side. Our worker Lambda will call STS to get the JWT and use that as authentication towards MongoDB, which use the public keys etc from STS to validate the user. For the MongoDB use case we need to map all of this to a user, but more about that config later.

Now, let's build!
Enable Outbound Identity Federation
Before we can us the Outbound Identity Federation we need to enable it, by default it's disabled. So head over the AWS Console and IAM > Account Settings.

Scroll down to the Outbound Identity Federation section and just click Enable.

Once enabled, you'll get your account-specific Token Issuer URL right away. Copy that, you'll need it later.

You can also enable it via CLI if you prefer that:
aws iam enable-outbound-web-identity-federationNow let us try this out and call STS to get an token, we run the CLI command below to get a token for our logged in principal.
aws sts get-web-identity-token --audience "my-api" --signing-algorithm RS256We will get a response like:
{
"WebIdentityToken": "Base64 Encoded Token",
"Expiration": "2026-03-19T08:20:06.177000+00:00"
}If we decode the WebIdentityToken, using a webpage like jwt.io we can see the different claims.
{
"aud": "my-api",
"sub": "arn:aws:iam::<account_id><iam_role>",
"https://sts.amazonaws.com/": {
"org_id": "<AWS_Org_Id>",
"aws_account": "<account_id>",
"ou_path": [
"...."
],
"original_session_exp": "2026-03-19T10:15:04Z",
"source_region": "eu-west-1",
"principal_id": "<iam_principal>",
"identity_store_user_id": "...."
},
"iss": "https://....tokens.sts.global.api.aws",
"exp": 1773908406,
"iat": 1773908106,
"jti": "..."
}Verifying the Token with API Gateway
With that working, let's build a simple API that a Lambda function calls, where we have a Lambda authorizer that validates the token.
We start by creating the caller function, this is rather straightforward, we call STS to get a token and then present that when calling the API.
import json
import logging
import os
import boto3
import requests
logger = logging.getLogger()
logger.setLevel(logging.INFO)
sts_client = boto3.client("sts")
API_ENDPOINT = os.environ["API_ENDPOINT"]
AUDIENCE = os.environ["AUDIENCE"]
def get_token():
response = sts_client.get_web_identity_token(
Audience=[AUDIENCE],
DurationSeconds=300,
SigningAlgorithm="RS256",
)
return response["WebIdentityToken"]
def handler(event, context):
try:
token = get_token()
response = requests.get(
API_ENDPOINT,
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
return {
"statusCode": response.status_code,
"body": json.dumps(
{
"api_status_code": response.status_code,
"api_response": response.json() if response.ok else response.text,
"request_id": context.aws_request_id,
}
),
}
except Exception as e:
return {
"statusCode": 500,
"body": json.dumps(
{
"message": "Error calling protected API",
"error": str(e),
}
),
}If we look at the get_token() function, it's dead simple, just three parameters and done. We don't need to fetch credentials from anywhere. The Lambda just says "give me a token for this audience" and STS signs one with the Lambda's own identity.
The Lambda Authorizer
The Lambda Authorizer attached to API gateway, this is where it start to become really interesting. What we will do is fetch the public keys from STS and then validate the token using a standard JWT library.
We create the discovery using issuer URL and append /.well-known/openid-configuration That tells us where we can find the public keys and what the issuer value should be. Then we create a PyJWKClient that handles fetching and caching keys.
import json
import logging
import os
import urllib.request
import jwt
logger = logging.getLogger()
logger.setLevel(logging.INFO)
AUDIENCE = os.environ["AUDIENCE"]
ISSUER_URL = os.environ["ISSUER_URL"]
DISCOVERY_URL = f"{ISSUER_URL}/.well-known/openid-configuration"
discovery_doc = json.loads(urllib.request.urlopen(DISCOVERY_URL).read())
JWKS_URI = discovery_doc["jwks_uri"]
EXPECTED_ISSUER = discovery_doc["issuer"]
jwks_client = jwt.PyJWKClient(JWKS_URI)
def handler(event, context):
try:
token = extract_token(event)
if not token:
return generate_policy("Deny", event["methodArn"])
signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=OIDC_AUDIENCE,
issuer=EXPECTED_ISSUER,
)
return generate_policy(
"Allow",
event["methodArn"],
principal_id=decoded.get("sub", "unknown"),
context={"sub": decoded.get("sub", ""), "iss": decoded.get("iss", "")},
)
except jwt.ExpiredSignatureError:
logger.warning("Token has expired")
except jwt.InvalidAudienceError:
logger.warning("Invalid audience claim")
except jwt.InvalidIssuerError:
logger.warning("Invalid issuer claim")
except Exception as e:
logger.error("Token verification failed: %s", e)
return generate_policy("Deny", event["methodArn"])The jwt.decode() function is the core of everything and will validate the actual tokens against the expected values, and throw an exception in case of a problem.
If it all passes, we allow the request and forward the sub claim so the backend knows who's calling.
Worth noting is that there's nothing AWS specific about this verification logic. This is just a standard JWT token validation.
Authenticating to MongoDB Atlas
Now let's put this into a real world use case, connecting towards a MongoDB Atlas cluster using tokens from IAM Outbound Federation, removing the need for storing passwords in Secrets Manager.
There is one very important thing to notice. For this to work you will need to run a MongoDB Atlas M10+ dedicated cluster with MongoDB 7.0.11 or higher. Workload Identity Federation is not supported on free or shared tier clusters (M0, M2, M5).
The idea is rather simple and straightforward. Our Lambda function call STS to get a token, the we present this token to Atlas instead of a username and a password. Atlas will validate the token, using standard flows, and maps the IAM Role to a database user.
Setting Up Atlas
The first step now is to setup things on the MongoDB side, login to your account and navigate to Federation under Identity and Access.

Under Federation select Identity Providers, and we'll start setting that up.

On the next screen select Workload Identity so we can start adding details.

On the configuration screen fill in name and description. The issuer URL is the one from the AWS IAM Console, that we saw earlier in this post. For audience set a value of your choice, important that it's the same on both MongoDB and AWS config (when calling STS). The audience must match. For user claim keep the default sub

Save and finish your setup and you should be presented with the overview screen.

Next we need to connect the identity provider we just created with our organization. Select Organizations in the menu and start to connect.

In the popup dialog select the provider we just created and click Connect.

In the next view we should see the overview of the connection.

Deploy test application
That was a lot of steps, but now we can deploy our test application and then do the final setup in Atlas before testing. We'll keep the testing infrastructure minimal. Just one Lambda function that with sts:GetWebIdentityToken permission:
Resources:
DatabaseOpFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${Application}-database-op
CodeUri: src/DatabaseOp/
Handler: database_op.handler
Environment:
Variables:
MONGODB_URI: !Ref MongoDbUri
OIDC_AUDIENCE: !Ref OidcAudience
Policies:
- Statement:
- Effect: Allow
Action:
- sts:GetWebIdentityToken
Resource: "*"
Outputs:
DatabaseOpFunctionRoleArn:
Description: IAM Role ARN for the Lambda function (use this in Atlas OIDC role mapping)
Value: !GetAtt DatabaseOpFunctionRole.ArnWe add the role ARN to the output of the stack, as we will need that for the final Atlas part. The code is split in the Lambda handler and a client that will handle connecting to the MongoDB database.
The pymongo driver has a great callback mechanism for OIDC. We give it a class, and the driver calls it whenever it needs a token. The driver hits fetch() on first connection and again when the token is about to expire. We set the value to 280 seconds, which is a bit under the actual 300, so there's a buffer.
Setting authMechanism="MONGODB-OIDC" tells the driver to skip username/password and use OIDC instead.
import os
import boto3
from pymongo import MongoClient
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackResult
sts_client = boto3.client("sts")
cached_client = None
def get_oidc_token():
audience = os.environ["OIDC_AUDIENCE"]
response = sts_client.get_web_identity_token(
Audience=[audience],
DurationSeconds=300,
SigningAlgorithm="RS256",
)
return response["WebIdentityToken"]
class AwsOidcCallback(OIDCCallback):
def fetch(self, context):
token = get_oidc_token()
return OIDCCallbackResult(access_token=token, expires_in_seconds=280)
def get_mongo_client():
global cached_client
if cached_client is not None:
return cached_client
uri = os.environ["MONGODB_URI"]
properties = {"OIDC_CALLBACK": AwsOidcCallback()}
cached_client = MongoClient(
uri,
authMechanism="MONGODB-OIDC",
authMechanismProperties=properties,
)
return cached_clientThen in our Lambda handler we create the MongoDB client and interacts with the database, in this simple example just inserting a document in an items collection.
def handler(event, context):
try:
client = get_mongo_client()
db = client["sample_db"]
collection = db["items"]
document = {
"message": "Hello from Lambda with OIDC auth",
"timestamp": datetime.now(timezone.utc).isoformat(),
"request_id": context.aws_request_id,
}
result = collection.insert_one(document)
doc = collection.find_one({"_id": result.inserted_id})
return {
"statusCode": 200,
"body": json.dumps({
"message": "Successfully connected to MongoDB Atlas with OIDC",
"insertedId": str(result.inserted_id),
}),
}
except Exception as e:
logger.error("Error: %s", e)
return {"statusCode": 500, "body": json.dumps({"error": str(e)})}Create a Database User in Atlas
Now we need to do the final step in Atlas, creating a database user and mapping that to the IAM Role ARN, so we can get permissions to interact with the database. Go to Database Access and add a new database user.

Select Federated Auth as the authentication method and set the username to the Lambda functions IAM Role ARN, fetch this from the stack output. Assign the user Read and write on any database permissions.

That role ARN will match the sub claim in the JWT. That's how Atlas knows which database user to use.
Allowlist network access
Before we test everything there is one more thing we need to do, we must allowlist network access for our Lambda function. Since we don't run in a VPC we must allow 0.0.0.0/0 in a production setup, don't do this, but for this test it's just fine.
Navigate to IP Access List in the menu

Then add 0.0.0.0/0, set it as temporary so it removed automatically.

That's it!! We can now use the Outbound Identity Federation in STS to authenticate towards MongoDB Atlas and can read and write data, no passwords, no secrets to manage!!
Testing
Time to do a small test, to do that just invoke the deployed Lambda function. Then move to the Data Explorer in Atlas and verify the data is there.

Final Words
I really like this pattern. No stored secrets. Tokens that expire in 5 minutes. You know exactly which IAM role made every connection. And it works with anything that speaks OIDC, not just MongoDB. Any external service that can verify OIDC tokens can use the exact same get_oidc_token() function. Same code, different destination.
Check out my other posts on jimmydqv.com and follow me on LinkedIn and X for more serverless content.
Now Go Build!
Test what you just learned by doing this five question quiz.
Scan the QR code below or click the link above.
Powered by kvist.ai your AI generated quiz solution!
