Networking & Content Delivery

Geo-block Content Using Amazon Location and Edge Services

Organizations require methods to restrict access to content to adhere to compliance and regulatory requirements, sanctions, privacy laws, territorial ownership rights, security controls, etc. One way that companies restrict access is by Geo-blocking – restricting access to a website or another piece of content based on a user’s location. A popular method of geo-blocking content is based on determining a user’s location using their IP address. However, this method is fairly simple to get around using technologies such as a Virtual Private Network (VPN).

In this walkthrough, I demonstrate how to geo-block access to a web application based on a user’s physical location rather than their IP-based location. This method lets you add geo-blocking restrictions on the server-side that can’t be bypassed with VPN alone.

Solution overview

Disclaimer: The sample code, software libraries, command line tools, proofs of concept, templates, or other related technology (including any of the foregoing that are provided by our personnel) are provided to you as AWS Content under the AWS Customer Agreement, or the relevant written agreement between you and AWS (whichever applies). You should not use this AWS Content in your production accounts, on production, or on other critical data. You are responsible for testing, securing, and optimizing the AWS Content, such as sample code, as appropriate for production grade use based on your specific quality control practices and standards. Deploying AWS Content may incur AWS charges for creating or using AWS chargeable resources, such as running Amazon EC2 instances or using Amazon S3 storage.

In this walkthrough, you deploy a web page on Amazon Simple Storage Service (Amazon S3) to send over location data (latitude and longitude) to an AWS Lambda function. This Lambda function will leverage Amazon Location Service to Reverse Geocode the user’s location and return a URL for redirection. Depending on the user’s location, they will be routed to an error page or forwarded to a web page running on Amazon Elastic Compute Cloud (Amazon EC2).

Walkthrough

Architecture DiagramFigure 1: Architecture Diagram

There are numbers on this diagram to highlight each step in the user flow:

  1. The User navigates to your domain in their browser
  2. The domain points to an alias record backed by an Amazon CloudFront
  3. The CloudFront distribution points to an S3 bucket.
  4. The S3 bucket includes some HTML and JavaScript files, which are returned to the user’s web browser
  5. The HTML Geolocation API is leveraged to get the users physical coordinates. These are sent to a Lambda function.
  6. The Lambda function calls Amazon Location, which reverse-geocodes the user’s location using the provided coordinates.
  7. A URL is returned to the HTML Page.
  8. If the user is in an “approved” geolocation, then they are redirected to an Amazon Application Balancer (ALB) sitting behind an AWS Web Application Firewall (WAF).
  9. If the user is making the request from the subdomain, then AWS WAF will allow the request to pass to the ALB. However, if the source of the request to the ALB isn’t the subdomain, then AWS WAF will reject the request and deny access.
  10. The ALB distributes requests to an EC2 Instance and a webpage is returned to the user

Prerequisites

For this walkthrough you should have:

The client application

A client application is needed to get the user’s physical location. For this walkthrough, you can use an HTML webpage hosted on Amazon S3. You’ll also use Route 53 for the Domain Name System (DNS). Then, you’ll leverage ACM to issue a new public Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificate for your domain. Lastly, you’ll use CloudFront to distribute the static web page to users.

Setting up Amazon S3

Step 1: Create an S3 Bucket | Documentation

Step 2: Create HTML and JavaScript Files
Using your preferred code editor, create the following two files:

Filename: geolocation.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Geolocation Request Page</title>
  </head>
  <body>
    <h1>Click the button below to provide your geolocation</h1>
    <p id="location"></p>
    <button onclick="getLocation()">Get Geolocation</button>
    <script src="/geolocation.js"></script>
  </body>
</html>

Filename: error.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Error</title>
  </head>
  <body>
    <h1>You are not in an allowed location. Access Denied</h1>
  </body>
</html>

Step 3: Upload the files to the S3 Bucket | Documentation

Setting up Route 53

Step 1: Create a Public Hosted Zone for Your Apex Domain | Documentation
This must be a domain that you own. If you don’t yet have a domain, then you can register a domain using Route 53.

Step 2: Create a Subdomain in Route 53 | Documentation
Use a subdomain to route users to the client application.

Setting up ACM

Step 1: Create an ACM Certificate for the Domain | Documentation

Make sure that you protect any subdomains by using the * syntax (see image). Leave everything else as default:

ACM Certificate Domains

Figure 2: ACM Certificate Domain Names

Step 2: Validate Domain Ownership
To validate the domain, click into it and navigate to the Domains panel. On the right-hand side, select the Create records in Route 53 button to automatically create the CNAME records in your domain’s hosted zone. After a few minutes, the status should change to issued. You can move on to the next section and verify this later.

Setting up CloudFront

Step 1: Create a Distribution | Documentation

Set the Origin domain to the S3 bucket created previously. Use the following settings:

  • S3 bucket access: Yes, use OAI (bucket can restrict access to only CloudFront)
  • Legacy Access Identities > Origin access identity (OAI): Create a new OAI. It should auto-populate the field. If not, then select it manually from the dropdown.
  • Bucket policy: Yes, update the bucket policy

Leave everything else as default and save. This will take ~15 minutes to provision.

Step 2: Edit General Distribution Settings
Click into the distribution that you just created and under the General tab, navigate to the Settings panel and select the Edit button. Apply changes to the following fields:

  • Alternate domain name (CNAME): [your subdomain]
  • Custom SSL certificate: [your SSL certificate from ACM]
  • Default root object: [the html file you created previously]

It will take ~5 minutes for these changes to deploy.

Step 3: Point your Subdomain to the CloudFront Distribution
Return to the Route 53 Hosted Zone for your subdomain and create a new ‘A’ record with the Alias toggle active. Use the following option:

  • Route traffic to: Alias to CloudFront distribution.
  • Select your CloudFront distribution ID from the dropdown.

Alias Record

Figure 3: Route 53 Alias Record

It will take ~5 minutes for this change to propagate out. Once it does, you can navigate to your subdomain in a web browser and see your HTML page:Geolocation Request Page

Figure 4: Geolocation Request Page

The back-end

Next, set up the back-end to receive geolocation coordinates and reverse-geocode them.

For this walkthrough, create a Place Index in Amazon Location to reverse-geocode the coordinates. Amazon Location is a fully-managed service that makes it easy for developers to add location functionality, such as maps, points of interest, geocoding, routing, tracking, and geofencing to their applications, without sacrificing data security, user privacy, data quality, or cost.

A Lambda function will receive the coordinates and make an API call to Amazon Location. If the user is in the correct location, then their request will be forwarded from the subdomain webpage to the apex domain webpage. The request must pass through AWS WAF, which will block any requests that don’t originate from the subdomain. If the request is not blocked, then it will reach the ALB, which will then forward the request to an EC2 instance.

Setting up Amazon Location

Step 1: Create a Place Index | Documentation
Use default settings. Make sure to note your place index name and ARN, as you’ll need them later.

Setting up Lambda

Step 1: Create a Lambda Function | Documentation
Make sure that the runtime is set to NodeJS 16. Leave all other options as default.

Step 2: Edit Permissions to Allow Amazon Location
Select the Configuration tab and select “Permissions” in the left panel. By default, the Lambda execution role can’t interact with your Place Index. Therefore, you must add the appropriate permissions. Select the role name to open AWS Identity and Access Management (IAM). Hit the ‘+’ sign next to the policy name and then select “Edit”. This will bring up the Visual Editor for adding/removing IAM permissions from this policy. Select “Add additional permissions”. Choose “Location” as the Service. Under “Actions”, select Read > SearchPlaceIndexForPosition. Under resources, select Edit next to place-index and give it the ARN of your place index. Then review policy and save the changes.

lambda-permissions-policy

Figure 5: Adding Minimum Location Permissions to Lambda Function IAM Policy

 

Step 3: Create a Lambda Function URL
A Lambda function URL is a dedicated HTTP(S) endpoint for your Lambda function. We create one to allow the front-end to send the coordinates to our Lambda function.

If needed, return to the Lambda function and select the Configuration tab. Select Function URL in the left-hand panel and select Create function URL. Use the following configuration:

  • Auth type: NONE (See considerations for this approach here)
  • Configure cross-origin resource sharing (CORS): checked
  • Allow origin: https://[your subdomain]
  • Allow methods: PUT

Leave everything else as default, and select Save

Now that you have a Lambda Function URL, create the following JavaScript file and upload it to your S3 bucket:

Filename: geolocation.js

let x = document.getElementById("location");

function getLocation() {
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(showPosition, showError);
  } else {
    x.textContent = "Geolocation is not supported by this browser.";
  }
}

function showPosition(position) {
  let lat = position.coords.latitude;
  let long = position.coords.longitude;
  let coordinates = {};

  console.log(lat);
  console.log(long);

  x.textContent = `Latitude: ${lat} Longitude: ${long}`;

  coordinates.latitude = lat;

  coordinates.longitude = long;

  console.log("coordinates: ", coordinates);

  //Send coordinates to AWS Lambda
  fetch(
    "[insert lambda function URL]",
    {
      method: "PUT",
      mode: "cors",
      body: JSON.stringify(coordinates),
    }
  )
    .then((response) => response.json())
    .then(
      (data) => (
        (x.textContent = `Latitude: ${lat} Longitude: ${long}`),
        console.log(data),
        window.location.assign(data)
      )
    );
}

function showError(error) {
  switch (error.code) {
    case error.PERMISSION_DENIED:
      x.textContent = "User denied the request for Geolocation.";
      break;
    case error.POSITION_UNAVAILABLE:
      x.textContent = "Location information is unavailable.";
      break;
    case error.TIMEOUT:
      x.textContent = "The request to get user location timed out.";
      break;
    case error.UNKNOWN_ERROR:
      x.textContent = "An unknown error occurred.";
      break;
  }
}

Make sure to replace the [insert lambda function URL] text with your actual Lambda Function URL.

Step 4: Edit Lambda Function Code
If needed, change to the Code tab in the Lambda function. Currently it’s using the default code. Here is the Lambda Function Code that you can use to reverse geocode coordinates:

Filename: index.js

//Define Libraries
const AWS = require("aws-sdk");
const location = new AWS.Location();

//Begin Function
exports.handler = async (event) => {
  //Set Variables
  let coordinates = JSON.parse(event.body);
  let latitude = coordinates.latitude;
  let longitude = coordinates.longitude;

  //Parameters for Location Service
  let params = {
    IndexName: "[your place index]" /* required */,
    Position: [longitude, latitude],
    MaxResults: 1,
  };

  //Reverse geocodes a given coordinate and returns a legible address. Logs to CloudWatch Logs
  location.searchPlaceIndexForPosition(params, function (err, data) {
    if (err) console.log(err, err.stack); // an error occurred
    else console.log(data.Results); // successful response
  });

  //Store legible address
  let locationData = await location
    .searchPlaceIndexForPosition(params)
    .promise()
    .then((data, err) => {
      return data;
    });

  //Filter for state. See CloudWatch logs for other JSON values
  let state = locationData["Results"][0]["Place"]["Region"];

  if (state === "New York") {
    let redirectURL = "https://[your apex domain]"; // Change the redirect URL to your apex domain

    const response = {
      statusCode: 302,
      statusDescription: "Found",
      body: JSON.stringify(redirectURL),
    };
    return response;
  }

  if (state !== "New York") {
    let redirectURL = "https://[your subdomain]/error.html"; // Change the redirect URL to your subdomain's error page
    const response = {
      statusCode: 302,
      statusDescription: "Found",
      body: JSON.stringify(redirectURL),
    };
    return response;
  }
};

Make sure to insert your own values for the place-index and both redirectURLs.

Setting up ALB and EC2

Step 1: Create Security Groups | Documentation

Create the ALB Security Group (SG) with the following settings:

  • Security group name: [choose a name]
  • VPC: [choose appropriate VPC]
  • Inbound Rules:
Type Source Source Value
HTTP Custom 0.0.0.0/0
HTTPS Custom 0.0.0.0/0

Create the Amazon EC2 SG with the following settings:

  • Security group name: [choose a name]
  • VPC: [choose same VPC as previous SG]
  • Inbound Rules:
Type Source Source Value
HTTP Custom ALB SG ID
HTTPS Custom ALB SG ID

 Step 2: Launch New Amazon EC2 Instance

Go to the Amazon EC2 console and select Launch an instance. Use the following parameters:

  • Name: [Relevant name]
  • Application and OS Images (Amazon Machine Image (AMI)): [Select Amazon Linux]
  • Key pair name – required: Proceed without a key pair
  • VPC: [Select same VPC as Security Groups]
  • Firewall (security groups): Select existing security group
  • Security groups: [Select EC2 SG created previously]
  • Advanced Details –> User data:
#!/bin/bash
sudo yum update -y
sudo yum install -y httpd
sudo systemctl start httpd
sudo systemctl enable httpd
sudo systemctl is-enabled httpd
sudo usermod -a -G apache ec2-user
sudo chown -R ec2-user:apache /var/www
sudo chmod 2775 /var/www && find /var/www -type d -exec sudo chmod 2775 {} \;
find /var/www -type f -exec sudo chmod 0664 {} \;
cat > /var/www/html/index.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome</title>
</head>
<body>
<h1>Looks like you're in the right location. Welcome!</h1>
</body>
</html>
EOF

This script updates the instance, installs an apache web server, and creates an index.html file with some basic content. Leave every other option as default and launch the instance.

Step 3: Create New Target Group and ALB
If needed, navigate to the Amazon EC2 console and select Load Balancers in the left-hand panel. Create the ALB and Target Groups by following the documentation here. For the ALB, change the following parameters:

  • ALB name: [Choose a relevant name]
  • VPC: [Select same VPC as EC2 Instance]
  • Mappings: [Select All]
  • Security groups:
    • Remove Default SG
    • Add ALB SG created in previous step
    • Listeners and routing:
Protocol Port Default Action
HTTP 80 Forward to: [Select your Target Group]
HTTPS 443 Forward to: [Select your Target Group]
    • Secure listener settings:
      • Security Policy: Leave Default
      • Default SSL/TLS certificate: From ACM: [Select your domain certificate]

Step 4: Create Alias Record in Route 53 | Documentation
To reroute the user to the apex domain’s web page, go to Route 53 and create an alias record in the apex domain’s hosted zone. Make sure that it points to the ALB.

Setting up AWS WAF

Step 1: Create a Web ACL

Open the AWS WAF console and select the Create web ACL button. Use the following settings:

  • Name: [Choose a name]
  • Resource type: Regional resources
  • Associated AWS resources:
    • Add AWS resources:
      • Resource type: ALB
      • Name: [Select your ALB]
      • Select the Add button
      • Select Next
  • Add rules > Add my own rules and groups
  • Name: [Choose a relevant rule name]
  • Inspect: Single header
  • Header field name: referrer
  • Match type: Starts with
  • String to match: https://[your subdomain].
  • Text transformation: Lowercase
  • Action: Allow
  • Select Add rule
  • Default web ACL action for requests that don’t match any rules: Block
  • Leave everything else default and select Create web ACL

Finishing up

Navigate back to your subdomain. Now when you select the button for the first time, the browser asks for permission to access your geolocation. Select allow. After a few seconds, it returns your coordinates and reroutes you based on your location. This might take a few seconds. Assuming that you didn’t change the Lambda function code, if you’re not in New York, then you should see an error page:

Error Page

Figure 6: Error Page

Otherwise you should be redirected to the apex domain without issue:

Apex Domain Webpage

Figure 7: Apex Domain Web Page

If you try to navigate directly to the apex domain, you should see this error, indicating that the request was blocked by AWS WAF:

blocked request

Figure 8: Blocked Request from AWS WAF

If you did not see the error page, try again in incognito mode as the apex domain web page may be cached in your browser.

You can edit the Lambda function to allow/block using other location parameters aside from state. Navigate back to the Lambda console and let’s investigate some of the code. The searchPlaceIndexForPosition function in this example returns the following JSON response body:

{
    Distance: double,
    Place: {
      AddressNumber: [string],
      Country: [string],
      Geometry: [Object],
      Interpolated: [true/false],
      Label: [address],
      Municipality: [string],
      PostalCode: [string],
      Region: [string],
      SubRegion: [string]
    }
  }

Region typically aligns with a US state if the coordinates are from the United States. Follow these steps to see how the JSON keys map to your location:

  • Navigate to your Lambda function.
  • Select the Monitoring tab and select View logs in CloudWatch.
  • Select the first item in Log stream.
  • One of the log events should include the location information from Amazon Location. As an example, here’s my location data (some information redacted for privacy).

CloudWatch Log for Lambda Function

Figure 9: CloudWatch Logs Geolocation Information

Cleaning up

To avoid incurring charges for running resources, as well as maintaining a clean environment, take the following actions on resources in your AWS account:

Conclusion

In this post, you learned how to leverage Amazon Location to reverse-geocode coordinates received from a client application and geo-block content based on a user’s physical location. Here, you did this based on the ‘Region’ key, but you can get much more granular; even down to a specific street address. You can even combine this functionality with CloudFront’s geolocation headers to control access to content based on a user’s physical location and their IP-based location. For more ideas on how you can leverage Amazon Location, visit our Front-End and Mobile blog site. If you have any other questions, post them on our forum, AWS re:Post.

Wasay Mabood

Wasay is a Partner Solutions Architect based out of New York. He works primarily with AWS Partners on migration, training, and compliance efforts but also dabbles in web development. When he’s not working with customers, he enjoys window-shopping, lounging around at home, and experimenting with new ideas.