Location-Based Access Control Made Easy with Next.js and IPInfo

gemanor

Gabriel L. Manor

Posted on July 18, 2023

Location-Based Access Control Made Easy with Next.js and IPInfo

In web applications, controlling access based on various factors is often necessary. One common requirement is restricting access to certain content or features based on a user’s IP address. This can be achieved using Permit.io’s low-code/no-code Authorization tool in conjunction with the IPInfo service. This article will explore how to implement IP-based access control in a Next.js application.

The overarching concept behind this demo will be controlling access to a specific subdomain. Our primary objective is to restrict access to the subdomain solely to users within our designated location. Users outside a defined location will be automatically redirected to the main page if they attempt to access the subdomain.

The tools we will be using

IPInfo is a popular IP address data provider with over 10 contextualized datasets derived from IP addresses. It provides information such as the country, city, anonymous IPs, and the organization associated with an IP. IPInfo’s API can be accessed via various programming languages, including JavaScript, with their Node.js library.

Permit.io is a flexible access control developer tool designed to simplify implementing various access control mechanisms in web applications. It allows you to define access rules based on roles or permissions and provides a simple permit.check()function to enforce them. With Permit, you can easily integrate access control into your Next.js applications.

In the following sections, we will guide you through setting up a Next.js project and implementing IP-based access control using Permit and IPInfo.

Prerequisites : This guide requires basic knowledge of Next.js and JavaScript.

*Important: * This demo app will focus purely on the functionality and not the UI we will see.

Let’s create a basic starter App in Next.js!

As a Developer Advocate at Permit.io, I have found that the most effective way to guide developers through the intricate realm of Authentication and Authorization is by demonstrating them through code and practical examples. Let’s dive into that.

To get started, you’ll need to have Node.js installed on your computer.

Then, follow these steps:

  1. Install the NextJS boilerplate, name your project, and select the setting that suits your development best. I always build my projects with the /src folder enabled and lintr installed.

    npx create-next-app@latest
    
  2. Navigate to the newly created directory.

    cd YOUR_PROJECT_NAME
    
  3. Run the development server.

    npm run dev
    

Now you should see your Next.js application running on http://localhost:3000. Congratulations!

supertokens-1.png

Creating a new page in Next.js

Once you have your repository open, we need to create a new folder under /src called /pages. Inside this folder, we can add paths on top of our URL that will display the relevant pages. Let’s create a file called /only-uk.

Inside the file, paste this simple code and see it in action.

import React from "react";

export default function UnitedKingdomOnly() {
 return (
  <h1>If you are currently in the UK you will be able to see this page.</h1>
 );
}
Enter fullscreen mode Exit fullscreen mode

It works :)

ip-info-1.png

Setting up Permit and our first policy

We will need to create an ABAC policy within Permit. ABAC (Attribute-based access control) is an authorization model that evaluates attributes (or characteristics), alongside roles, resources, and actions to determine access. The purpose of ABAC is to allow users to define more complex access-control rules to prevent other users from unauthorized actions — those that don’t have “approved” characteristics as defined by an organization’s security policies.

For this demo, we can create three roles, a simple Admin role, an Admin from the UK and an Admin from Poland. As you can see, the two latter roles have IP location-based attributes.

First, go ahead a create an account at https://app.permit.io. You will be presented with the onboarding, but once you enter your organization name, you can just skip the setup.

Once in the dashboard, navigate to the policy screen and manage our roles.

supertokens-9.png

It’s time to create our first role. Each role will have specific rules associated with it, of what a user can and cannot do. We must create the Admin role first, as it will later serve as a building block for our ABAC conditions.

If you want to learn more about building an ABAC condition — check out our guide here.

ip-info-2.png

ip-info-3.png

Success!

Next, we need to create the two other roles with attributes (or otherwise known in Permit as User Sets.

User Sets are groups of users that adhere to pre-defined attribute-based conditions. These are conditions based on individual user characteristics.

However, before we jump into creating the User Sets, we need to define the attributes that will also be used for building our conditions. In this case, the attribute we will need to add will be ip_location.

Attributes are values that serve as a declaration of what the condition rule will be.

Now — ABAC is not enabled by default within Permit, so we need to switch the toggle. In the policy screen, at the top, you will see a tab called ABAC Rules. Click on it and toggle ABAC on.

ip-info-4.png

Now as we navigate to the Users panel in Permit, a new tab will appear called Attributes. Click on it, and add a new user attribute called ip_location with the type of String. We need to define the type — this is a declaration of the value we will compare the attribute to.

ip-info-5.png

While we are in the Users panel — let’s add a sample user. In general, this would be the user and their unique ID that you would get from the JWT (JSON Web Token) upon successful authentication, but for this demo we will just fake that process and pretend it has already happened.

If you need suggestions on the best Authentication Providers to work with, just message us on Slack, and we will be more than happy to suggest some.

If you would like to check out a guide on how to add a user to Permit after successful authentication with Auth0, check out the article here.

ip-info-6.png

You can name the user however you’d like.

Now that we have the attribute and user set up, we can continue creating the two other roles (with conditions) — which means we will be creating User Sets.

Navigate to the User panel and into the ABAC Rules tab. Here, let’s create a new User Set.

uk-admin.png

Our UK Admin role needs extra conditions, so let’s create a condition group. We stated that the ip_location is equal to the country code UK.

ip-info-7.png

Now you can do the same for the Polish Admin role.

poland-admin.png

Well done! You should now see two User Sets.

ip-info-8.png

Great work so far. The last part that we need to create in the no-code dashboard is a resource and the actions we want to allow the roles to perform on the resource. The resource will be our restricted page called only-uk , and the action will be view. You can have as many actions as you want, but in order to keep this demo very simple, we will just create one.

In order to create a resource, navigate to the policy panel and click on the Resources tab and create a resource.

A resource is the target object we want to authorize access to. It’s what the user will or won’t be able to perform actions on.

ip-info-9.png

Once the resource is created and as we navigate to the main Policy Editor, you should see something like this:

abac-policy-undelected.png

Our policy is finished. Now, let’s enforce it in the UI. The only role that should be able to view the restricted page is UK Admin. Let’s tick the view box.

abac-selected.png

Voila! Our Permit Policy configuration is finished!

Installing dependencies

Next, install the necessary dependencies by running the following command:

npm install permitio ipinfo swr --save
Enter fullscreen mode Exit fullscreen mode

Initializing the Permit instance and running the PDP

When writing the code, we will need to consider two things:

  1. Creating an API endpoint that will handle the authorization and IP fetching.
  2. Adjust the restricted page to show a message based on the result of the enforcement.

Inside of our /pages folder, let’s create another folder called /api , and inside that folder, we need to create a file — let’s call it restrict.js. This will become the URL endpoint that we call from our frontend, and it will appear under the endpoint path /api/restrict.

Inside the restrict folder, we need to import Permit and initialize the instance.

import { Permit } from "permitio";

const permit = new Permit({
 pdp: "http://localhost:7766",
 // your API Key
 token: process.env.PERMIT_API_KEY,
});
Enter fullscreen mode Exit fullscreen mode

To be able to use ABAC within Permit, we have to deploy the PDP (Policy-Decision-Point).

Just run these two commands in your terminal to download the container and run it:

$ docker pull permitio/pdp-v2:latest
$ docker run -p 7766:7000 --env PDP_API_KEY=<YOUR_API_KEY> permitio/pdp-v2:latest
Enter fullscreen mode Exit fullscreen mode

Make sure to replace the API Key inside the Permit instance and the docker run command. You can find your PermitAPI Key by clicking on your profile image and copying the key.

supertokens-8.png

Coding the Permit ABAC enforcement check

In order to restrict access, we need to include the permit.check() function in our code to check a specific user against our configured policy.

Let’s add the below code to our restrict.js file.

import { Permit } from "permitio";

const permit = new Permit({
 pdp: "http://localhost:7766",
 // your API Key
 token: process.env.PERMIT_API_KEY,
});

#################### ADD CODE BELOW ###################

export default async function enforceAccess(req, res) {

 const allowed = await permit.check(
  {
   key: "demo_user@gmail.com",
   attributes: {
    ip_location: SOME_LOCATION,
   },
  },
  "view",
  {
   type: "only-uk",
   tenant: "default",
  }
 );

 res.status(200).send({ allowed });

 return allowed;
}
Enter fullscreen mode Exit fullscreen mode

Inside the permit.check() function, we pass three parameters: the user object with attributes that we are checking for, the action being performed and then resource object, passing in the resource name and the tenant.

A tenant is as a silo of resources and users; which in policy terms means only users within a tenant can act on the resources within the tenant. Tenants are isolated from one another.

Now if we look at the code, as part of the passed-in attributes, we are missing the IP location — which we are currently not handling at all. Not to worry, we have IPInfo to help us!

Creating an account with IPInfo

To utilize the IPInfo service, you must sign up for an account and acquire an Access Token. IPInfo offers 50,000 free geolocation requests every month. Visit theIPInfo page, sign up for a free account, and enter the dashboard:

Screenshot 2023-06-13 at 15.01.37.png

In general, IPInfo is very simple to set up, and you can start using it within minutes. If you check out their developer documentation, they give you many ways to run a simple request to fetch the user country and IP.

We will be using the async/await Fetch API to get the user country code.

As we are dealing with an access token here, it’s always best practice to store these tokens in an .env file to ensure you don’t accidentally share sensitive information.

ip.png

I have an .env.local file setup with the below configuration:

PERMIT_API_KEY=
IP_INFO_TOKEN=
Enter fullscreen mode Exit fullscreen mode

Fetching a users IP address and Country Code

Now let’s add a function into the /restrict.jsfile to fetch the IP of the current user trying to access the restricted /only-uk page.

const fetchLocationByIP = async () => {
 const request = await fetch(
  `https://ipinfo.io/json?token=${process.env.IP_INFO_TOKEN}`
 );
 const jsonResponse = await request.json();

 console.log(jsonResponse.ip, jsonResponse.country);

 return jsonResponse.country;
};
Enter fullscreen mode Exit fullscreen mode

The above code will fetch the user details, extract the IP and country, and return the two-letter country code.

Now we need to pass the returned country code to theip_location attribute. The whole code should look something like the example below.

Making it all work together

import { Permit } from "permitio";

const permit = new Permit({
 pdp: "http://localhost:7766",
 // your API Key
 token: process.env.PERMIT_API_KEY,
});

const fetchLocationByIP = async () => {
 const request = await fetch(
  `https://ipinfo.io/json?token=${process.env.IP_INFO_TOKEN}`
 );
 const jsonResponse = await request.json();

 console.log(jsonResponse.ip, jsonResponse.country);

 return jsonResponse.country;
};

export default async function enforceAccess(req, res) {

 // Getting the country code and storing in a variable
 const IPlocation = await fetchLocationByIP();

 const allowed = await permit.check(
  {
   key: "demo_user@gmail.com",
   attributes: {
    // Passing the variable to the ip_location attribute
    ip_location: IPlocation,
   },
  },
  "view",
  {
   type: "only-uk",
   tenant: "default",
  }
 );

 res.status(200).send({ allowed });

 return allowed;
}
Enter fullscreen mode Exit fullscreen mode

Great! Now, as a final step, let’s edit the/only-uk page to handle the response correctly and display the appropriate message.

import React from "react";
import useSWR from "swr";

export default function UnitedKingdomOnly() {
 const fetcher = (url) => fetch(url).then((res) => res.json());
 const { data, error } = useSWR("/api/restrict", fetcher);

 if (error) return <div>Failed to load.</div>;
 if (!data) {
  return <div>Loading...</div>;
 } else {
  if (data.allowed) {
   return (
    <div>
     If you are currently in the UK you will be able to see this page.
    </div>
   );
  } else {
   return <div>User is not based in the UK.</div>;
  }
 }
}
Enter fullscreen mode Exit fullscreen mode

As I am currently in the UK myself, so if I try to access the page, I will get the below message:

ip-info-1.png

However, now I will change my location to Poland using a VPN, and as I refresh the page and try to reaccess the page, we will see this:

Screenshot 2023-06-13 at 14.53.33.png

Hurray! Our application now uses both IPInfo to fetch the user's current location, and Permit to enforce the access based on the location.

Learn more about Permit or IPInfo — or access the whole code repository for this project here.

💖 💪 🙅 🚩
gemanor
Gabriel L. Manor

Posted on July 18, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related