AWS Storage Blog
How to develop a user-facing data application with IAM Identity Center and S3 Access Grants (Part 2)
This post is Part 2 of a two-part blog post series that will take you, an application developer, through the process of configuring and developing a data application that authenticates users with Microsoft Entra ID and then uses S3 Access Grants to access data on those users’ behalf.
Part 1 of this series gave an overview of how the new Trusted Identity Propagation feature of IAM Identity Center, together with S3 Access Grants, simplify both access management and audit for a data lake in S3 with end-user access. In Part 1, we took you through the process of configuring the application in Microsoft Entra ID and in IAM Identity Center. We introduced the concept of a “trusted token issuer” in IAM Identity Center: the connective tissue in AWS that allows your application to act in AWS on behalf of a user that authenticated to your identity provider.
Here, in Part 2, we will show you, with example code, what you need to do to develop this data application.
Recap from Part 1: The data application
The data application we’re describing here supports single sign-on authentication with a Microsoft Entra ID tenant. It allows its authenticated users to interact with data, for example to request visualizations of the data, and accesses the data on behalf of each user through S3 Access Grants. We’ll take you through the detailed steps of this workflow below.
Figure 1 – Data application built on S3 Access Grants with Trusted Identity Propagation. Permission mappings are defined directly in S3 Access Grants.
For our example, we’re going to walk through the steps your application will take when a fictional user, named Mateo Jackson, signs in and requests a pie-chart visualization of this month’s tomato sales. Mateo is a member of the Red Project Group in the Entra ID directory; this group is supposed to have read access to data on sales of all things that are red. Therefore, your S3 Access Grants instance might have a grant such as this one, where the grantee GUID, 0374d892-
… is the identifier of the Red Project Group in IAM Identity Center.
Figure 2: S3 Access Grant that grants read-only access to a directory group
We will assume that your application is running in AWS account 111111111111
. We will also assume that your AWS Organization’s instance of IAM Identity Center runs in AWS account 222222222222
. Other configurations are possible, as described in Part 1 of this series.
Overview of data-access flow in our application
The application we’re describing in this blog post is an “Enterprise application” in Microsoft Entra ID. That means it has the ability to authenticate your end-users by redirecting them to Entra ID’s sign-in page. It will make use of Trusted Identity Propagation and S3 Access Grants to access data in S3, on behalf of a specific authenticated user, for example to display a visualization of the data.
Figure 3: Summary of user-facing flow in your data application. Users log in and then request a view of the data through your application.
At a high level, the user’s interaction with data in your application will work like this:
- Login with Entra ID. Because this application is an Enterprise Application in your Entra ID tenant, it will initiate a single-sign-on flow (i.e. redirecting the user’s web browser), so that the user can authenticate.
- Token exchange with IAM Identity Center. To use the signed-in user’s identity with AWS services (specifically, with S3 Access Grants), your application will exchange the identity token from Entra ID with IAM Identity Center, for an identity token that will be recognized within AWS.
- User interaction with your application. Your application presents its users with a means of interacting with data. For example, it might provide a visualization requested by the user that will require it to read some data from S3 on the user’s behalf.
- Request access to data from S3 Access Grants. Your application will make an API call to S3 Access Grants to request temporary access to the data in S3, on behalf of the signed-in user.
- Read S3 objects. Using the result of the previous step, your application will read or write the data in S3, and present the view of the data that the user had requested. We will show you how these S3 object reads are attributed directly to the authenticated end-user in the resultant CloudTrail data events for S3.
All of these steps will be taken by your application, so let’s look at the code that you’ll need to write to do it.
These examples make use of the AWS SDK for Javascript v3, but similar patterns can be implemented in any AWS SDK.
Step 1: User login
The user workflow begins when the user, Mateo, visits your web application and signs in.
Figure 4: Normal user authentication flow for an Entra ID application: User redirects to Identity Provider and then back to the application.
AWS services are not involved in this interaction. Rather, this is a standard sign-in flow that you might implement as an enterprise application in Entra ID. If your identity provider is Microsoft Entra ID, for example, you might use the Microsoft Authentication Library for Javascript. Entra ID supports a number of different authorization patterns, and the one you choose will depend on how your application is structured. If, for example, your application’s business logic is server-side, you might choose to implement a “confidential client” that handles an authorization code grant. In that case, the user-facing flow would look like this:
A. Mateo navigates to your application in his web browser.
B. The application redirects him to the Entra ID tenant, where he’s prompted to sign in, MFA, etc.
C. Entra ID redirects his web browser back to your application with an authorization code, which your application exchanges for an Entra ID identity token.
Other authentication patterns will look somewhat different to the user. Regardless of which you use, at the end of the process, your application will have obtained an identity token, in the form of a JSON Web Token (JWT), issued and signed by Entra ID. This token represents the identity of the authenticated user.
We can base64-decode
the identity JWT from Microsoft and take a look at some of its relevant fields: the aud
claim is the enterprise application id assigned by Microsoft. The iss
claim is the issuer, namely the Microsoft Entra ID. The oid
claim is the user’s unique id in Entra ID, which also appears as the “external ID” of the same user in our synchronized IAM Identity Center directory.
// Decoded identity token from Entra ID. Identifies the signed-in user.
// {
// …
// "idTokenClaims": {
// "aud": "46600a38-…",
// "iss": "https://login.microsoftonline.com/7139b665-.../v2.0",
// …
// "name": "Mateo Jackson",
// "oid": "f96ba61f-…",
// "preferred_username": "mateo.jackson@example.com",
// …
// },
// …
// }
Step 2: Token exchange with IAM Identity Center
Because you’re about to make requests to AWS on behalf of this authenticated user, you will need to obtain an identity token that AWS services will be able to recognize.
Figure 5: Application exchanging the original identity token for the user (from Entra ID), for an identity token that will be recognized by AWS services.
So, in this step, your application makes an API call, CreateTokenWithIAM
, to IAM Identity Center’s OIDC service. The input is the identity token issued by Microsoft Entra ID, and the output will be a new identity token, issued by IAM Identity Center. The WithIAM
part of the API name refers to the fact that IAM Identity Center is using IAM authentication and authorization to verify that this caller – the IAM role for your application, which we refer to as Application IAM role here – can make requests for this Application as identified by the clientId
parameter.
import { SSOOIDCClient, CreateTokenWithIAMCommand } from "@aws-sdk/client-sso-oidc";
const jwt = require('jsonwebtoken');
const ssooidc = new SSOOIDCClient(config);
// entraIdToken is the identity token you obtained from Microsoft Entra ID
// when the user signed in.
const createTokenWithIAMCommand = new CreateTokenWithIAMCommand({
clientId: 'arn:aws:sso::222222222222:application/ssoins-12345678/apl-87654321',
grantType: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: entraIdToken
});
const tokenExchangeResponse = await ssooidc.send(createTokenWithIAMCommand);
// Response:
// {
// …
// "tokenType": "Bearer",
// "expiresIn": 3600,
// "idToken": "eyJhbGci…", // Base64-encoded
// "refreshToken": "aorAAAA…"
// …
// }
// You will need to decode the IAM Identity Center-issued identity token for
// the next step
const identityCenterTokenDecoded = jwt.decode(tokenExchangeResponse.idToken);
// Base64-decoding the value of idToken:
// {
// …
// "sts:identity_context": "AQoJb3Jp…
// "aws:instance_account": "222222222222",
// "iss": "https://identitycenter.amazonaws.com/ssoins-12345678",
// …
// }
The identity token in the response to CreateTokenWithIAM
is a JSON Web Token (JWT) from IAM Identity Center that will be usable in requests to AWS services. Of particular significance here is the sts:identity_context
field, an opaque value that you will use in Step 4 to obtain an IAM session that represents Mateo’s identity as you make the data access request to S3 Access Grants on his behalf.
CreateTokenWithIAM
is an AWS API call, which means you need to call it with an IAM principal that has sufficient permissions to do so. The above code will typically run under the IAM role associated with your application’s compute environment, which we’ve named Application role in this blog post. For example, if your application is deployed on an AWS Lambda function, the Application role will be that Lambda function’s execution role. This IAM role will need the following trust policy and permissions; notice that it does not need permission to take any S3 actions.
// Configuration for the Application IAM role
// Assume-Role trust policy document for the Application IAM role
// (The Application IAM role is a Lambda function execution Role)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}]
}
// IAM permissions needed for the Application IAM role.
// The first statement allows the token exchange with IAM Identity Center.
// The second statement allows the AssumeRole call we will make in Step 4.
{
"Effect": "Allow",
"Action": "sso-oauth:CreateTokenWithIAM",
"Resource": "arn:aws:sso::222222222222:application/ssoins-12345678/apl-87654321"
},
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111111111111:role/IdentityBearerRole"
}
Step 3: User interaction
The user Mateo, having signed in, starts interacting with the application. For example, if this application draws pie charts, Mateo might ask for a pie chart infographic of last month’s tomato sales. Your application would use a catalog or some other source of information to determine that tomato sales results can be found at the S3 prefix s3://example-bucket/projects/red/tomatoes/
. Mateo is a member of the Red Project Group in our directory, which, as we saw above, has a grant for read access to s3://example-bucket/projects/red/*
; that grant will cover the tomato sales data.
Figure 6: Authenticated user interacts with your application and makes a request that will require reading some data in S3. (AWS services not involved in this step.)
Because you are using S3 Access Grants, your Application IAM role does not need any direct permission to read and write objects in S3. Rather, it will be obtaining temporary access to that S3 prefix by way of S3 Access Grants, so that it can have permission to exactly the right sets of data for each authenticated user. So, before reading the data from S3 that will populate the tomato pie chart, your application will first need to make an API request to S3 Access Grants, on behalf of Mateo, to request temporary read access to this prefix in S3. We will see the details of that interaction in the next step.
Step 4: Application requesting access to data on behalf of the user.
To gain access to the data in S3 on tomato sales on behalf of the authenticated user Mateo, your application needs to request temporary access to that data from S3 Access Grants.
Figure 7: Requesting temporary access to data in S3, on behalf of the authenticated user. First, you will call AWS STS to assume an “Identity Bearer” IAM role, supplying the identity token as additional user context. The IAM session that results securely identifies the authenticated user. With that IAM session, you will then make a request to S3 Access Grants, which will evaluate whether the user should be able to read data at this S3 prefix.
Breaking down the two request/response interactions above:
A. STS AssumeRole request. Your application needs an IAM session with which to call S3 Access Grants on behalf of the authenticated user. You will make use of a new feature of the AWS Security Token Service (STS) that allows identity context to be included in an IAM session. We call this IAM role the Identity Bearer role.
B. STS AssumeRole response. STS will return temporary credentials (Access Key Id, Secret Access Key, Session Token) to your application. This IAM session represents both your application running in account 111111111111
as well as its authenticated user’s identity.
C. S3 Access Grants GetDataAccess request. Using the credentials for that Identity Bearer IAM role that represents the authenticated user, your application will make a request to the S3 Access Grants GetDataAccess
API, asking for read access to s3://example-bucket/projects/red/tomatoes/
.
D. S3 Access Grants GetDataAccess response. As discussed earlier, your S3 Access Grants instance has a grant allowing the Red Project directory group read access to everything that matches the S3 prefix s3://example-bucket/projects/red/*
. Mateo is a member of the Red Project Group, and the request matches that prefix; therefore, he should get access. S3 Access Grants’ success response to your application will include a new set of IAM session credentials. These credentials belong to an IAM role that you would have previously associated with your S3 Access Grants instance. For more information on how S3 Access Grants work, read Scaling data access with Amazon S3 Access Grants.
Let’s go over these two AWS interactions in detail.
4A/B. Assuming an identity-bearer session
Your application runs under the IAM role that we’re calling Application IAM role in this blog post. This, typically, will be something like a Lambda function execution IAM role, an EC2 instance IAM Profile role, a IAM roles Anywhere IAM role (if running on non-AWS compute), or similar. Starting with this Application IAM role, you are going to assume a second IAM role, which we will call the Identity Bearer IAM role.
The Identity Bearer IAM role’s trust policy document will look like the below. This trust policy allows the Identity Bearer IAM role to be assumed by the Application role, as long as additional user identity context – a new STS feature for use with Trusted Identity Propagation – is included in the request.
// Assume-Role trust policy document for Identity Bearer IAM role
// (Assumable by Application IAM role, typically in the same account).
//
// This trust policy mandates that additional identity context be included
// in the AssumeRole request.
// The sts:SetContext permission is necessary when passing user identity
// context.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "111111111111"
},
"Action": [
"sts:AssumeRole",
"sts:SetContext"
],
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:role/ApplicationRole"
},
"StringEquals": {
"sts:RequestContext/identitycenter:InstanceArn": "arn:aws:sso:::instance/ssoins-12345678"
}
}
}]
}
Because the Identity Bearer IAM role will be used to make a request to S3’s GetDataAccess
, it will need the permission to the s3:GetDataAccess
action. Note that the Identity Bearer IAM role does not need permission to access objects in S3 directly. Instead, the success response from GetDataAccess
will include a new set of IAM session credentials that will have direct access to the S3 objects.
// IAM permissions needed for the Identity Bearer IAM role:
// Requests to S3 Access Grants
{
"Effect": "Allow",
"Action": "s3:GetDataAccess",
"Resource": "arn:aws:s3:REGION:111111111111:access-grants/default"
}
The following demonstrates how you will assume the Identity Bearer IAM role with the additional user context from Trusted Identity Propagation.
// Assume the Identity Bearer IAM role, with additional context to identify
// the authenticated user.
//
// identityCenterTokenDecoded is the decoded version of the JWT token that
// your Application Role obtained in the previous step. As a reminder, that
// token was in the success response from IAM Identity Center's
// CreateTokenWithIAM API request, which exchanges the user's original Entra
// ID identity token for one that can be used with AWS services, as you are
// about to do here.
import {STSClient, AssumeRoleCommand} from "@aws-sdk/client-sts";
const sts = new STSClient();
const assumeRoleCommand = new AssumeRoleCommand({
RoleArn: 'arn:aws:iam::111111111111:role/IdentityBearerRole',
RoleSessionName: 'identity-bearer-for-mateo-jackson',
ProvidedContexts: [{
ProviderArn: 'arn:aws:iam::aws:contextProvider/IdentityCenter',
ContextAssertion: identityCenterTokenDecoded['sts:identity_context']
}]
};
const assumeRoleResponse = await sts.send(assumeRoleCommand);
// Response from AssumeRole:
// {
// …
// "Credentials": {
// "AccessKeyId": "ASIAEXAMPLE",
// "SecretAccessKey": "SECRET",
// "SessionToken": "FwoGZXIvYX…",
// "Expiration": …
// },
// "AssumedRoleUser": {
// "AssumedRoleId": "AROAEXAMPLE:identity-bearer-for-mateo-jackson",
// "Arn": "arn:aws:sts::111111111111:assumed-role/IdentityBearerRole/identity-bearer-for-mateo-jackson"
// }
// }
const identityBearerSessionCredentials = {
accessKeyId: assumeRoleResponse.Credentials.AccessKeyId,
secretAccessKey: assumeRoleResponse.Credentials.SecretAccessKey,
sessionToken: assumeRoleResponse.Credentials.SessionToken
};
4C/D: Requesting access to data from S3 Access Grants
Using this IAM session for the Identity Bearer role, which represents both your application running in account 111111111111
as well as the authenticated user Mateo Jackson, you will then make the request to S3 Access Grants to request temporary access to the data on his behalf. You will make that request like this:
import {S3ControlClient, GetDataAccessCommand} from "@aws-sdk/client-s3-control";
// You will be making this request to S3 Access Grants using your IAM session
// for the Identity Bearer Role. Thus, your S3Control client needs to use
// the IAM credentials for the STS session you just obtained for the Identity
// Bearer IAM role
const s3Control = new S3ControlClient({
credentials: identityBearerSessionCredentials
});
// Request access to the data using this S3 client
const getDataAccessCommand = new GetDataAccessCommand({
AccountId: '111111111111',
Target: 's3://example-bucket/project/red/tomatoes/',
Permission: 'READ'
});
const getDataAccessResponse = await s3Control.send(getDataAccessCommand);
// Response:
// {
// "Credentials": {
// "AccessKeyId": "ASIAEXAMPLE2",
// "SecretAccessKey": "SECRET",
// "SessionToken": "IQoJb3JpZ2lu…",
// "Expiration": …
// },
// "MatchedGrantTarget": "s3://example-bucket/project/red/*"
// }
const dataAccessSessionCredentials = {
accessKeyId: getDataAccessResponse.Credentials.AccessKeyId,
secretAccessKey: getDataAccessResponse.Credentials.SecretAccessKey,
sessionToken: getDataAccessResponse.Credentials.SessionToken
};
If successful, you will get an API response from S3 Access Grants’ GetDataAccess
API that contains:
- A new set of IAM session credentials. These credentials are for the IAM role that is associated with S3 Access Grants. Like your Identity Bearer IAM role session, these credentials will have the authenticated user’s identity embedded in them. (Under the hood, S3 Access Grants did a token exchange with IAM Identity Center similar to the one you did in Steps 2 and 4A/B. That is why these resulting IAM session credentials also carry Mateo’s identity.)
- Information about the access scope of this session (
MatchedGrantTarget
). Notice that the grant that your request matched in S3 Access Grants wass3://example-bucket/project/red/*
, even though you asked for a subset of that,s3://example-bucket/project/red/tomatoes/
. The IAM credentials in this success result therefore have read-only access (s3:GetObject
,s3:ListBucket
) to the data unders3://example-bucket/project/red/
. This is useful because, until the credentials expire, your application will be able to (and should) reuse the credentials for potentially many requests to S3 in this set of data; your application typically would not make a new request to S3 Access Grants for eachGetObject
request.
In your application, the S3 bucket “example-bucket” and your application happen to be in the same AWS account, 111111111111
. However, S3 Access Grants supports GetDataAccess
requests from other AWS accounts, by way of a resource-based IAM policy on the S3 Access Grants instance. If example-bucket were, instead, in account 333333333333
, your application would be making an API call to S3 Access Grants in account 333333333333
. So long as the resource-based policy on your S3 Access Grants instance in account 333333333333
allows your caller, which is the Identity Bearer IAM role from account 111111111111
, and so long as it contains a grant matching the authenticated user’s request for data, that access would be allowed too. You can read more about cross-account access to S3 Access Grants in the documentation.
Summarizing this step: You just made an API call to S3 Access Grants, using a session on an IAM role that we called the Identity Bearer role. This IAM session represents the authenticated user’s identity to S3 Access Grants. S3 Access Grants matched your application’s request to one of its grants and returned a different set of temporary IAM credentials. In the next step, you will use this new set of IAM credentials to read data from S3.
Step 5: Read objects from S3
Figure 8: Application uses the temporary credentials from S3 Access Grants to read the requested data from S3.
Now that S3 Access Grants has provided you with the credentials for an IAM session that can make S3 object API requests on behalf of the authenticated user, your application can read the data it needs from S3 to serve the right results, for example to draw its pie chart.
import {S3Client, ListObjectsV2Command, GetObjectCommand} from "@aws-sdk/client-s3";
// You will be making this request to S3 using the credentials that S3 Access
// Grants' GetDataAccess API returned to you in the previous section
const s3 = new S3Client({
credentials: dataAccessSessionCredentials
});
// You can make as many read requests to S3 as you want, until the credential
// expiration time
await s3.send(new ListObjectsV2Command({
Bucket: 'example-bucket',
Prefix: 'project/red/tomatoes/'
}));
…
await s3.send(new GetObjectCommand({
Bucket: 'example-bucket',
Key: 'project/red/tomatoes/roma.csv'
}));
await s3.send(new GetObjectCommand({
Bucket: 'example-bucket',
Key: 'project/red/tomatoes/cherry.csv'
}));
…
The credentials returned from GetDataAccess
should be used for repeated S3 object operations (GetObject
, etc.) until they expire.
As mentioned previously, the integration between S3 Access Grants and IAM Identity Center Trusted Identity Propagation also simplifies audit tasks. The CloudTrail data events for these S3 object API calls will reflect Mateo’s original identity. That makes it easier for you to directly audit who accessed this data; it is right there in each CloudTrail data event.
// Example CloudTrail data event resulting from the above workflow
{
…
"userIdentity": {
"type": "AssumeRole",
"principalId": "AROAEXAMPLE:access-grants-01234567-89ab-cdef…",
"accountId": "111111111111",
…
"sessionContext": {
"sessionIssuer": {
"type": "Role",
…
"arn": "arn:aws:iam::111111111111:role/s3-access-grants-role"
…
}
…
},
"onBehalfOf": {
"userId": "b3743f38-…",
"identityStoreArn": "arn:aws:identitystore::222222222222:identitystore/d-123456780a"
}
}
…
The onBehalfOf
field above indicates that there was a user identity in this IAM role session. The GUID (userId
) b3743f38-
… is the identifier of user Mateo Jackson, in IAM Identity Center. This request is tied directly to the identity of the user to whom the data is being served.
In summary
We just walked through all of the details of your user-facing data application. When a user visits your application, the application drives the following interactions:
- Login: Redirect the user to the Entra ID tenant to sign in (no AWS interactions)
- Token Exchange: Present the user’s identity token from Entra ID to IAM Identity Center, in exchange for an IAM Identity Center token that can be used within AWS
- User data request: Determine what data your application needs to access for the user (no AWS interactions)
- Request access to the data from S3 Access Grants: You used the IAM Identity Center identity token to assume an IAM role that we called the Identity Bearer Role. The resulting IAM session represented both your application and the authenticated user. You used that IAM session to make a request to S3 Access Grants, for the requested data on behalf of the authenticated user. S3 Access Grants’
GetDataAccess
API response returned a different set of IAM credentials with direct permission to the requested data on behalf of the authenticated user. - Read data from S3: Using the IAM credentials emitted from S3 Access Grants, your application reads objects from S3 on behalf of the user. The CloudTrail data events for S3 directly reflect the user’s identity.
Figure 9: Summary of end-user data interaction workflow in our application.
Review of IAM interactions
If you’re anything like me, you like to understand the details of every IAM interaction in your environments. There were three IAM roles involved in your application’s workflow, and we discussed each as it came up. This section reviews each of those IAM roles, the actions it takes, and what permissions it minimally needs.
Figure 10: View of your data application, from the perspective of the three IAM roles involved in the workflow: Application role, Identity Bearer role, and S3 Access Grants role.
- The Application IAM role is an IAM role for your application.
- Assumed by:
lambda.amazonaws.com
, if the application is deployed on an AWS Lambda function. (Similarlyec2.amazonaws.com
if deployed on EC2, etc.) - Actions taken: An API request to IAM Identity Center to exchange the Entra ID identity token for an IAM Identity Center identity token that can be used within AWS.
- Minimal permissions needed:
sso-oauth:CreateTokenWithIAM
- Assumed by:
- The Identity Bearer IAM role is an IAM role whose sessions will include the authenticated user’s identity. You used this IAM role to make requests to S3 Access Grants for access to the data.
- Assumed by: the Application IAM role, with the additional user-identity context from IAM Identity Center.
- Actions taken: An API request to S3 Access Grants
- Minimal permissions needed:
s3:GetDataAccess
- The S3 Access Grants IAM role is an IAM role that you would have configured as part of your S3 Access Grants setup. You would have associated this IAM role with an S3 Access Grants location. The data-access IAM sessions were be from this IAM role.
- Assumed by:
access-grants.s3.amazonaws.com
- Actions taken: Object API calls to S3, for example
s3:GetObject
. Each session’s permissions will be scoped precisely to the S3 prefix of the grant that the user matched. - Minimal permissions needed: this will vary by your exact S3 Access Grants configuration, but in general will have S3 read and write access to some or all of your S3 buckets. You can find some recommended policy examples at IAM roles for S3 Access Grants.
- Assumed by: