Read on my blog
Read time: 6 minutes
Lean manufacturing focuses on minimizing waste while simultaneously maximizing productivity.
If you apply the same mindset and look at our applications today, you will find many CRUD APIs that add little value beyond authentication and authorization. That is, they provide authorised access to a database and ensure a user can’t access someone else's data.
However, AWS services such as DynamoDB and S3 already have authorization control through AWS IAM. So what if we remove the API layer altogether and let the frontend application talk to AWS services directly?
This is not only possible but can be cost-effective and secure, thanks to AWS Cognito Identity Pools and DynamoDB’s leading key condition [1].
In this post, I will show you how. And I will explain when you should consider this radical approach.
You can try out our live demo app here [2] and see the code here [3].
Cognito Identity Pools lets you federate identity authentication to an identity provider such as Cognito User Pool and issue AWS credentials to your users so they can access AWS services directly.
It will validate the token. Make sure it’s valid, hasn’t expired and that the user hasn’t been signed out globally. And issue temporary AWS credentials for a pre-configured IAM role.
Cognito Identity Pools can also issue temporary AWS credentials for unauthenticated users. This is often used for:
However, for this blog post, we are only interested in the authenticated use case.
For the demo app, we will use Cognito User Pool as the identity provider. The frontend would:
The Cognito Identity Pool uses the Cognito User Pool as the identity provider. A default authenticated IAM role is used for all authenticated users.
CognitoIdentityPool:
Type: AWS::Cognito::IdentityPool
Properties:
IdentityPoolName: FeToDDBDemoIdentityPool
AllowUnauthenticatedIdentities: false
CognitoIdentityProviders:
- ClientId: !Ref CognitoUserPoolClient
ProviderName: !GetAtt CognitoUserPool.ProviderName
ServerSideTokenCheck: true
CognitoIdentityPoolRoleAttachment:
Type: AWS::Cognito::IdentityPoolRoleAttachment
Properties:
IdentityPoolId: !Ref CognitoIdentityPool
Roles:
authenticated: !GetAtt CognitoIdentityPoolRole.ArnThe key to ensuring the user can not access other people’s data is in this IAM role’s policy.
CognitoIdentityPoolRole:
Type: AWS::IAM::Role
Properties:
RoleName: FeToDDBDemoAuthenticatedRole
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: cognito-identity.amazonaws.com
Action:
- sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
cognito-identity.amazonaws.com:aud:
Ref: CognitoIdentityPool
Policies:
- PolicyName: FeToDDBDemoAuthenticatedRolePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
Resource: !GetAtt DynamoDBTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- "${cognito-identity.amazonaws.com:sub}"
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject*
Resource:
- !Sub ${S3Bucket.Arn}/${cognito-identity.amazonaws.com:sub}/test.jsonThere are several things worth noting here:
1. The IAM role’s assume role policy. This policy only allows the aforementioned Cognito Identity Pool to assume this role.
2. The dynamodb:LeadingKeys condition associated with the DynamoDB actions. This condition allows users to access only the items where the partition key matches their Identity ID.
The Identity ID can be retrieved by calling Cognito Identity Pool’s GetId API [4]. It looks like this: us-east-1:310d28ad-a894–4ce1-b787-b3023ca2094c.
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
Resource: !GetAtt DynamoDBTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- "${cognito-identity.amazonaws.com:sub}"3. The Resource associated with the S3 actions. This allows the users to access only the object with the key:
${cognito-identity.amazonaws.com:sub}/test.json
Where ${cognito-identity.amazonaws.com:sub} is their Identity ID.
- Effect: Allow
Action:
- s3:PutObject
- s3:GetObject*
Resource:
- !Sub ${S3Bucket.Arn}/${cognito-identity.amazonaws.com:sub}/test.jsonYou can relax this to allow the users to access any objects in their dedicated folder.
Resource:
- !Sub ${S3Bucket.Arn}/${cognito-identity.amazonaws.com:sub}/*We can securely allow the frontend application to access AWS resources directly using these fine-grained access controls.
In the frontend application, we need to first authenticate the user against the Cognito User Pool.
const signInResult = await Auth.signIn({
username: email.value,
password: password.value
})Once the user is signed in, we can fetch the user’s ID token from their auth session.
const session = await Auth.fetchAuthSession()As mentioned above, the user is only allowed to act on his or her data in DynamoDB and S3.
To find the user’s Identity ID, we need to call the GetId API [4] with the user’s ID token.
const getIdResp = await cognitoClient.send(new GetIdCommand({
IdentityPoolId: import.meta.env.VITE_COGNITO_IDENTITY_POOL_ID,
Logins: {
[import.meta.env.VITE_COGNITO_PROVIDER_NAME]: session.tokens.idToken.toString()
}
}))Next, we get the temporary AWS credentials from Cognito’s GetCredentialsForIdentity API [5]. We need the user’s Identity ID as well as the ID token.
const getCredResp = await cognitoClient.send(new GetCredentialsForIdentityCommand({
IdentityId: getIdResp.IdentityId,
Logins: {
[import.meta.env.VITE_COGNITO_PROVIDER_NAME]: session.tokens.idToken.toString()
}
}))Finally, we can use these credentials to initialize the DynamoDB and S3 clients.
const dynamoDBClient = new DynamoDBClient({
region: import.meta.env.VITE_AWS_REGION,
credentials: {
accessKeyId: getCredResp.Credentials.AccessKeyId,
secretAccessKey: getCredResp.Credentials.SecretKey,
sessionToken: getCredResp.Credentials.SessionToken
}
})
const documentClient = DynamoDBDocument.from(dynamoDBClient)
const s3Client = new S3Client({
region: import.meta.env.VITE_AWS_REGION,
credentials: {
accessKeyId: getCredResp.Credentials.AccessKeyId,
secretAccessKey: getCredResp.Credentials.SecretKey,
sessionToken: getCredResp.Credentials.SessionToken
}
})I have put together a live demo app [2] for you to try it out yourself.
Once logged in, you can use the buttons to read and write data in a DynamoDB table and S3.
The “authorized” buttons act on the user’s data, while the “unauthorized” buttons attempt to act on another user’s data.
As you can see, the IAM role only allows the frontend application to access data that belong to the logged-in user.
You are free to poke around in the source code as well. You can find both the backend and frontend code in this repo [3].
As you can see, it’s possible to securely let the frontend application talk to AWS resources directly.
This can be a very cost-effective solution for projects where reducing infrastructure costs is a top priority.
This approach eliminates the need for API Gateway and Lambda functions, thereby cutting down on costs associated with these services. It allows scalable and secure data access, where users can only access their data.
With this approach, you can also improve end-to-end latency. Because we have removed the overhead and potential bottlenecks (e.g. concurrency limits or cold start penalties) from API Gateway and Lambda.
While the benefits are significant, it’s crucial to be aware of the potential challenges and risks associated with this approach.
Here are some reasons why you shouldn’t use this approach:
In addition to the above, this approach also introduces many challenges to the development process. For example,
As we’ve explored in this post, directly accessing AWS services from frontend applications can offer significant benefits in terms of cost efficiency and performance. This approach eliminates the need for intermediaries like API Gateway and Lambda, thereby streamlining your architecture.
However, it’s crucial to balance these advantages against the many potential drawbacks we discussed above.
In my opinion, for 97% of applications out there, this approach is a premature optimization.
But for the other 3% of applications, this can be a good solution to minimize waste while simultaneously maximizing cost-efficiency and performance.
Remember, the best solutions are those tailored to specific challenges and goals. As you navigate the world of AWS and serverless, please don’t hesitate to reach out to me for support.
If you want to learn more about building serverless applications for the real world, then why not check out my next workshop [6]? The next cohort starts on January 8th, so there is still time to sign up and level up your serverless game in 2024!
As a subscriber, you can also get 25% off when you sign up with the code "NIEUWJAAR24". But hurry, this offer expires on 2nd January 2024.
[1] Using IAM policy conditions for fine-grained access control
[2] Live demo app
[3] The demo project source code
[4] Cognito Identity Pool’s GetId API endpoint
[5] Cognito Identity Pool’s GetCredentialsForIdentity API endpoint
[6] Production-Ready Serverless workshop
Join 17K readers and level up you AWS game with just 5 mins a week.
AI agents can now scan an entire open-source codebase for exploitable vulnerabilities in hours. Frontier models carry the complete library of known bug classes in their weights. So you can simply point an AI agent at a codebase and tell it to find zero-days. This isn't theoretical. Willy Tarreau, the HAProxy lead developer, reports that security bug reports have jumped from 2–3 per week to 5–10 per day. Greg Kroah-Hartman, the Linux kernel maintainer, described what happened: "Months ago, we...
Lambda Durable Functions makes it easy to implement business workflows using plain Lambda functions. Besides the intended use cases, they also let us implement ETL jobs without needing recursions or Step Functions. Many long-running ETL jobs have a time-consuming, sequential steps that cannot be easily parallelised. For example: Fetching data from shared databases/APIs with throughput limits. When data needs to be processed sequentially. Historically, Lambda was not a good fit for these...
Step Functions is often used to poll long-running processes, e.g. when starting a new data migration task with Amazon Database Migration. There's usually a Wait -> Poll -> Choice loop that runs until the task is complete (or failed), like the one below. Polling is inefficient and can add unnecessary cost as standard workflows are charged based on the number of state transitions. There is an event-driven alternative to this approach. Here's the high level approach: To start the data migration,...