Securing an endpoint used to be hard. Nowadays, with the cloud, it’s quite easy. You only need to know how! Assume you have files on S3 that you like to share. You could make the object publicly available. This would allow your users to download the file using their browsers simply. If you need to scale it, you can add CloudFront. This would cache the content closer to your users, making sure that your users have the best performance. But what if you want to control who can download the file? For this, you will need authentication and authorization.
Authentication vs Authorization
Authentication is all about identifying who you are. First, we need to make sure that we know who the user is. Once we establish who the user is, we can see if the user can access the content. The latter is authorization. AWS has a service called Cognito that allows you to manage a pool of users. These users can originate from other sources like Google, Facebook, and your own identity provider. Or, if you don’t want to use identity providers, you can also create users directly in the user pool.
You can also create groups, and based on these groups, you can manage the authorization. For example, you could make a group called developers. All users within this group should be allowed to fetch the build report hosted on S3.
Building the endpoint
With the Cognito User Pool in place, we need a way to validate the user during the request. I am using an Application Load Balancer to invoke a Lambda function. This function can then check if the user can access the report. The logic is quite simple: if the user is part of the developer’s group, the user can read the report.
In this case, we can use the native Cognito integration of the application load balancer. What this will do is the following:
If the user is unauthenticated, navigate to the Cognito-hosted UI. If you use your identity provider, you will be redirected to your company’s login page. If you host the users from the user pool, a login form is shown to the user. After the user has logged a redirect, the user is now authenticated. The load balancer will now invoke the target group with the request.
We also want to check if the user is in the developer group. We will use a Lambda function to check this. The following code would do the trick:
import base64
import json
from typing import List
def decode(data: str) -> dict:
return json.loads(base64.b64decode(data.split('.')[1]).decode('utf-8'))
def resolve_groups(groups: str) -> List[str]:
return list(map(lambda group: group.strip(), groups[1:-1].split(',')))
def handler(event, context) -> dict:
code = 403
description = "403 Access Denied"
body = "Access Denied"
user = decode(event["headers"]["x-amzn-oidc-data"])
groups = resolve_groups(user.get('custom:groups', '[]'))
if 'developers' in groups:
code = 200
description = "200 OK"
body = f"Hi {user.get('name')}, you should be able to download the report"
return {
"statusCode": code,
"statusDescription": description,
"isBase64Encoded": False,
"headers": {"Content-Type": "json; charset=utf-8"},
"body": body,
}
The listener on the application load balancer and the user pool client can be configured as follows:
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: arn:aws:acm:eu-west-1:111122223333:certificate/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
DefaultActions:
- AuthenticateCognitoConfig:
OnUnauthenticatedRequest: authenticate
Scope: openid
UserPoolArn: arn:aws:cognito-idp:eu-west-1:111122223333:userpool/eu-west-1_xXXXxxxx
UserPoolClientId: !Ref UserPoolClient
Order: 1
Type: authenticate-cognito
- Order: 2
TargetGroupArn:
Ref: LambdaTarget
Type: forward
UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
AllowedOAuthFlows:
- code
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- profile
- phone
- email
- openid
- aws.cognito.signin.user.admin
CallbackURLs:
- https://<MyDomainName>/oauth2/idpresponse
ClientName: MyClient
GenerateSecret: true
LogoutURLs:
- https://<MyDomainName>/logout
SupportedIdentityProviders:
- COGNITO
UserPoolId: eu-west-1_xXXXxxxx
You also need to setup the lambda function as followed:
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
Description: Allow HTTPS access
FromPort: 443
IpProtocol: tcp
ToPort: 443
VpcId: !Ref VPCAsParameter
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com
ManagedPolicyArns:
- !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
- !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
Code:
S3Bucket: my-bucket
S3Key: path/to/code.zip
Handler: index.handler
Role: !GetAtt LambdaRole.Arn
Runtime: python3.12
VpcConfig:
SecurityGroupIds:
- !GetAtt LambdaSecurityGroup.GroupId
SubnetIds:
- !Ref Subnet1
- !Ref Subnet2
- !Ref Subnet3
LambdaPermissions:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt LambdaFunction.Arn
Principal: elasticloadbalancing.amazonaws.com
LambdaTarget:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn:
- LambdaPermissions
Properties:
TargetType: lambda
Targets:
- Id: !GetAtt LambdaFunction.Arn
You will be prompted to log in when you navigate to the load balancer. Afterward, you can see that the lambda function was invoked. This is a very simple example, but the idea is that you can extend the lambda function with the logic you need. For example, you could create a pre-signed URL for the report on S3 and redirect the user directly to that URL. This will then download the report automatically.
Conclusion
Securing access to your S3 files doesn’t have to be complicated. By leveraging AWS Cognito, an Application Load Balancer, and a simple Lambda function, you can control exactly who gets access to your files—without exposing them to the public. With this setup, authentication is handled seamlessly, and authorization is as simple as checking group memberships. From here, you can expand the functionality further, such as generating pre-signed URLs for downloads or adding more granular permissions. The cloud makes it easy—you just need to know how!
Photo by Pixabay
The post Securing S3 Downloads with ALB and Cognito Authentication appeared first on Xebia.
Top comments (0)