Knowing a user's physical location is crucial for many applications.
- Streaming services like Netflix and Spotify enforce licensing agreements and apply content restrictions based on the user's geographical location.
- Dating sites like Tinder connect people living close by.
- Local marketplaces fight fake listings and reviews by checking for location consistency.
- Online gambling websites need to know your location to ensure jurisdictional compliance.
- Online stores and services apply regional pricing discounts to make their products more accessible globally.
However, bypassing these restrictions and taking advantage of location-based features is not hard. Turning on your VPN is enough to fool most websites, as any of the 17.1 thousand VPN ads on YouTube would promptly demonstrate for you.
In this article, we will dig deeper into how location spoofing via VPN works and how to detect it. You will learn to use Fingerprint VPN Detection to detect if your visitors are trying to spoof their location and prevent them from applying regional discounts to their carts.
How do websites know your location?
Web applications usually determine your location by looking up your IP address in an IP geolocation database. Several companies compile and update these databases using data from internet service providers (ISPs), network infrastructure, and voluntary contributions from users.
How does a VPN change your location?
When you turn on a VPN, you insert an intermediary between your device and the Internet.
- Instead of accessing a website directly, the VPN server receives your HTTP request and sends it to the website.
- The website sends the response to the VPN server, and the VPN server sends it to you.
The website only sees the VPN server's request and its IP address. Most VPN providers allow you to choose the location of the used VPN server, so you can appear to be anywhere in the world to the websites you visit.
This is often emphasized as one of the major benefits of using a VPN. VPN services have legitimate uses, including information security. But they can also be used to bypass security measures, defeat fraud protections, and take unfair advantage of location-based features or pricing.
Even more generally, fraudsters want to hide their location and cover their tracks no matter what they are doing. So VPNs and location spoofing indirectly enable all forms of malicious activity, not only location-related fraud.
How can Fingerprint detect VPN usage?
There are three main ways Fingerprint can detect visitors using a VPN:
- The browser timezone doesn't match the location inferred from the visitor's IP address.
- The visitor's IP address is found in a database of known VPN servers.
- The browser runs on a different operating system than the operating system inferred from the HTTP request network signature.
See our article on how VPN detection works for a more in-depth look into various techniques.
Note: Fingerprint also uses other proprietary methods to detect VPN usage for mobile devices. Other mobile Smart Signals also detect various forms of tampering that can be used to spoof one's location, such as rooted, jailbroken, Frida-instrumented, or emulated devices.
Tutorial: Detect VPN usage to prevent location spoofing
In this tutorial, you will learn to integrate Fingerprint into your website and use it to detect if your visitors are using a VPN to spoof their location. Using a checkout page as an example, you will prevent VPN users from applying regional discounts. You will also learn to use Sealed Client Results to minimize latency and protect your Fingerprint integration from client-side tampering.
You can try the final result on our website. The code snippets in the article are simplified for readability but the full code is open-sourced on GitHub.
1. Optional: Enable Sealed results
Fingerprint VPN detection is available as a Smart signal. The JavaScript agent only receives the visitor ID, request ID, and other metadata on the client. The full identification event, including Smart Signals, is only available in a server environment. There are several ways to retrieve the VPN detection result on the server:
- Use the
requestId
property returned to the JS agent, pass it to your server, then use the Fingerprint Server API to retrieve the full identification event, including Smart Signals. - Use Webhooks to receive identification events as they happen, using the
requestId
to link them with client requests. - Use Sealed Client Results to receive the full identification event in the JavaScript agent in encrypted form and decrypt it on the server.
You can use any of these to:
- Validate the authenticity of the visitor ID and other information sent from the client.
- Get the full identification event, including Smart Signals.
Sealed results have the following advantages over Server API and Webhooks:
- Reduced latency — Unsealing the result happens on your server; no third-party network requests are necessary.
- Increased secrecy — The payload is fully encrypted with a symmetric key, making it impossible for a malicious actor to read or modify the results on the client.
Sealed results make integrating Fingerprint results into your application faster, safer, and more straightforward. They are currently only available for the Enterprise plan. If you want to use Sealed results, contact our support team to enable them for your Enterprise account.
This tutorial will demonstrate how to use Sealed results, but also how to fall back to the Server API if necessary. If you are not interested in Sealed results, you can skip that part and only implement the Server API integration.
2. Identify visitors requesting a regional discount
When a visitor attempts to activate a regional discount, identify them with Fingerprint and send the result to your server.
First, sign up for a Fingerprint account and install the Fingerprint JavaScript agent on your website. If you are using a frontend framework like React, Angular, or Vue, we recommend using one of our frontend libraries, which have caching and other useful features built in.
// Initialize the agent.
const fpPromise = import(
'https://metrics.yourdomain.com/web/v3/<your-public-api-key>'
).then((FingerprintJS) =>
FingerprintJS.load({
endpoint: 'https://metrics.yourdomain.com',
})
)
Note: We recommend routing requests to Fingerprint CDN and API through your domain for production deployments, as shown above. This prevents disruption from ad blockers and improves accuracy. You can learn multiple ways to do this in our guide on Protecting the JavaScript agent from ad blockers.
Let's use a checkout screen as an example:
When a user tries to activate regional pricing, make a visitor identification request and send the requestId
to your server. If you are using Sealed results, also send the sealedResult
.
async function activateRegionalPricing() {
// Collect browser signals and request visitor identification
// from the Fingerprint API. The response contains a `requestId`
// and `sealedResult` if Sealed results are enabled
const { requestId, sealedResult } = await (await fpPromise).get()
// Pass the `requestId` and `sealedResult` to your server
const response = await fetch(`/vpn-detection/api/activate-ppp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requestId, sealedResult }),
})
}
3. Retrieve and validate Fingerprint results
Use either requestId
or sealedResult
on the server to get the full identification result, including VPN detection, and validate its authenticity. If provided data is corrupted or inconsistent, return an error instead of activating regional pricing.
export async function POST(req: Request) {
const { requestId, sealedResult } = await req.json()
// Get the full identification result and check its authenticity
const fingerprintResult = await getAndValidateFingerprintResult({
requestId,
req,
sealedResult,
})
if (!fingerprintResult.okay) {
return NextResponse.json(
{
severity: 'error',
message: fingerprintResult.error,
},
{ status: 403 }
)
}
// More checks below...
}
Optional: Decrypt sealed results
If you have Sealed results enabled, you can get the full identification result by decrypting the provided sealedResult
value.
You will need to use a symmetric encryption key, which you can create in the Fingerprint Dashboard.
Note: You can only create the encryption key if your application has Sealed results enabled. When an encryption key is activated, the JavaScript agent removes everything except requestId
and sealedResult
from the response payload. Make sure that there is nothing left on your client that depends on the original (unencrypted) payload before activating your encryption key.
To decrypt the sealed result, we recommend using one of our Server SDKs, which all provide a helper function to do so. Here is an example using the Node SDK:
import {
DecryptionAlgorithm,
unsealEventsResponse,
EventResponse,
} from '@fingerprintjs/fingerprintjs-pro-server-api'
export const getAndValidateFingerprintResult = async ({
requestId,
req,
sealedResult,
}) => {
let identificationEvent: EventResponse | undefined
/**
* If `sealedResult` was provided, try to decrypt it.
**/
if (sealedResult) {
try {
identificationEvent = await unsealEventsResponse(
Buffer.from(sealedResult, 'base64'),
[
{
key: Buffer.from(process.env.YOUR_ENCRYPTION_KEY, 'base64'),
algorithm: DecryptionAlgorithm.Aes256Gcm,
},
]
)
if (
identificationEvent.products?.identification?.data?.requestId !==
requestId
) {
return {
okay: false,
error:
'Sealed request ID does not match provided request ID, potential spoofing attack',
}
}
} catch (error) {
console.error(
`Decrypting sealed result failed. Falling back to Server API. Error: ${error}`
)
}
}
// ...
}
If decrypting the result fails for any reason, you can use the provided requestId
to retrieve the full identification result from the Server API, as shown in the next section.
Retrieve results from Server API
On the server, use the provided requestId
to get the full identification result from Fingerprint Pro Server API. If the requestId
is not found or the event is inconsistent, return an error instead of activating regional pricing.
You can call the Server API REST endpoint directly or use one of our Server SDKs. Here is an example using the Node SDK:
import {
FingerprintJsServerApiClient,
isEventError,
} from '@fingerprintjs/fingerprintjs-pro-server-api'
// ...
// If `sealedResult` was not provided or unsealing failed, use Server API to get the identification event.
if (!identificationEvent) {
try {
const client = new FingerprintJsServerApiClient({
apiKey: process.env.SERVER_API_KEY,
})
identificationEvent = await client.getEvent(requestId)
} catch (error) {
if (isEventError(error) && error.status === 404) {
return {
okay: false,
error: 'Request ID not found, potential spoofing attack.',
}
}
return { okay: false, error: String(error) }
}
}
const identification = identificationEvent.products?.identification?.data
// More checks below...
return { okay: true, data: identificationEvent }
Verify the authenticity of the identification event
There are several useful integrity checks you can do on the identification event to detect client-side tampering. For example, an attacker might have previously generated a valid request ID using your Public API key. Check the freshness of the identification request to prevent replay attacks.
// Identification event must be a maximum of 3 seconds old
if (Date.now() - Number(new Date(identification.time)) > 3000) {
return {
okay: false,
error: 'Old identification request, potential replay attack.',
}
}
Make sure the identification request comes from the same IP address as the discount request.
// This is an example of obtaining the client's IP address.
// In most cases, it's a good idea to look for the right-most
// external IP address in the list to prevent spoofing.
if (request.headers['x-forwarded-for'].split(',')[0] !== identification.ip) {
return {
okay: false,
error:
'Identification IP does not match request IP, potential spoofing attack.',
}
}
The identification request should originate from a trusted production website. Check that both the discount request origin and the identification request origin match your domain.
const ourOrigins = ['https://yourdomain.com']
const visitorDataOrigin = new URL(identification.url).origin
// Confirm that the request is from a known origin.
if (
visitorDataOrigin !== request.headers['origin'] ||
!ourOrigins.includes(visitorDataOrigin) ||
!ourOrigins.includes(request.headers['origin'])
) {
return {
okay: false,
error:
'Visit origin does not match request origin, potential spoofing attack.',
}
}
// If all checks pass, return the identification event back to the request handler.
return { okay: true, data: identificationEvent }
4. Prevent VPN users from activating regional pricing
Having retrieved the identification event (either using Sealed results or Server API) and validated its authenticity, you can check if the visitor is using a VPN.
const vpnDetection = fingerprintResult.data.products?.vpn?.data
if (vpnDetection?.result === true) {
return NextResponse.json(
{
severity: 'error',
message: `It seems you are using a VPN. Please turn it off and use a regular local internet connection before activating regional pricing.`,
},
{ status: 403 }
)
}
The VPN Detection result also includes metadata about which detection methods returned a positive result.
"vpn": {
"data": {
"result": true,
"originTimezone": "Europe/Prague",
"originCountry": "unknown" // Not yet supported for browsers
"methods": {
"timezoneMismatch": true,
"publicVPN": false,
"auxiliaryMobile": false, // Irrelevant for browsers, mobile SDKs only
"osMismatch": true
}
}
If the visitor is not using a VPN, let them apply their regional discount using the location provided by Fingerprint.
const location = fingerprintResult.data.products?.ipInfo?.data?.v4?.geolocation
const discount = getRegionalDiscount(location.country.code)
return NextResponse.json({
severity: 'success',
message: `Success! We have applied a regional discount of ${discount}%. With this discount your purchase will be restricted to ${location.country.name}.`,
data: { discount },
})
Explore Our VPN Detection Demo
Visit the VPN Detection Demo we built to demonstrate the concepts above. You can explore the open-source code on Github or run it in your browser with CodeSandbox. The core of the use case is implemented in this component and this endpoint.
Conclusion
Fingerprint's unique visitor identification and device intelligence platform enables companies to tackle fraud head-on by offering real-time insights into user behavior.
If you want to secure your application against VPN usage, we encourage you to sign up for a free trial. Our team can also provide tailored solutions to meet your security needs and help you fortify your services against location spoofing and other malicious online activity.
FAQ
Each of the used VPN detection methods has its blind spots.
A timezone mismatch is prone to false negatives if your VPN server is in the same timezone as you. You might also encounter false positives if you use mobile data with your local service provider abroad. Public VPN IP databases are not suitable for detecting private self-hosted and corporate VPNs. Operating system mismatch is very accurate for Windows devices but might be less accurate for other OS combinations.
However, by combining all of these methods, Fingerprint can detect a significant portion of VPN usage, and we are continuously working on improving the detection coverage and accuracy.
VPNs have allowed fraudsters to post fake rental and marketplace listings in remote locations they don't actually reside in. Steam had to disable its gift card functionality to limit regional pricing abuse. Tinder-swindler-style fraudsters have used location spoofing inside dating applications to appear as residents of high-net-worth areas. This increases credibility and makes it easier to find lucrative victims. Pokémon Go players have used location spoofing to access rare game content without actually being in the corresponding physical location.