DEV Community

Cover image for Securing S3 Downloads with ALB and Cognito Authentication
Joris Conijn for AWS Community Builders

Posted on • Originally published at xebia.com on

Securing S3 Downloads with ALB and Cognito Authentication

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,
    }

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)