Location-Based Access Control Made Easy with Next.js and IPinfo
- Share:
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:
- 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 andlintr
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!
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>
);
}
It works :)
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.
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.
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.
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.
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.
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.
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
.
Now you can do the same for the Polish Admin
role.
Well done! You should now see two User Sets.
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.
Once the resource is created and as we navigate to the main Policy Editor, you should see something like this:
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.
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
Initializing the Permit instance and running the PDP
When writing the code, we will need to consider two things:
- Creating an API endpoint that will handle the authorization and IP fetching.
- 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,
});
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
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.
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;
}
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 the IPinfo page, sign up for a free account, and enter the dashboard:
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.
I have an .env.local
file setup with the below configuration:
PERMIT_API_KEY=
IP_INFO_TOKEN=
Fetching a users IP address and Country Code
Now let’s add a function into the /restrict.js
file 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;
};
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;
}
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>;
}
}
}
As I am currently in the UK myself, so if I try to access the page, I will get the below message:
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:
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.
Written by
Filip Grebowski
Developer Advocate at Permit.io, Software Engineer, and YouTube Creator