- Published on
Identify Users Using Browser Fingerprinting
- Authors
- Name
- Parminder Singh
You've probably seen an email from your bank, Gmail, or Netflix that says, "We noticed a login from a new device.". To achieve this, they are tracking your device and notify you when a new device is used to log in. In this blog, I'll discuss browser fingerprinting - one of the simplest techniques that can be used to identify user's device. This technique can be used for security, analytics, personalization, fraud prevention, etc.
Browser fingerprinting collects publicly available browser and device information to generate a unique signature per device. Assuming your app is already covered by a Privacy Policy and Terms of Use that inform users about such tracking, here are some data points you can collect to create a unique fingerprint.
Each of these pieces of information may not be unique by itself, but when combined, they can create a unique identifier for the device. Also, each item can be thought of to have an importance/entropy/weight, based on how unique it is. For example, the user agent string may not be very unique, but the WebGL fingerprint is.
The list below is by no means exhaustive, but gives a good starting point.
Basic Browser Info
Basic browser information like user agent, language, platform, CPU cores, screen resolution, timezone, etc.
const browserInfo = {
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cpuCores: navigator.hardwareConcurrency,
screenResolution: `${window.screen.width}x${window.screen.height}`,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}
Canvas Fingerprint
The canvas fingerprint is generated by drawing text on a canvas element and then extracting the image data. Different devices and browsers may render the same text differently.
async function getCanvasFingerprint() {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.textBaseline = 'top'
ctx.font = '14px Arial'
ctx.fillText('fingerprint', 2, 2)
return canvas.toDataURL()
}
WebGL Fingerprint
WebGL (Web Graphics Library) lets browsers render 3D graphics, and fingerprinting it reveals details about a device’s GPU, driver, and graphics stack — often unique to each machine.
function getWebGLInfo() {
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl')
if (!gl) return null
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
return {
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL),
}
}
Audio Fingerprint
The audio fingerprint is generated by creating an audio context and analyzing the frequency data of a sound wave.
async function getAudioFingerprint() {
const ctx = new (window.AudioContext || window.webkitAudioContext)()
const oscillator = ctx.createOscillator()
const analyser = ctx.createAnalyser()
oscillator.connect(analyser)
oscillator.start()
const data = new Uint8Array(analyser.frequencyBinCount)
analyser.getByteFrequencyData(data)
oscillator.stop()
return data.slice(0, 10).join(',')
}
Fonts and Plugins
The fonts and plugins installed in the browser can also be used to create a unique fingerprint.
function getFontsAndPlugins() {
const fonts = []
const plugins = []
if (window.FontFace) {
document.fonts.forEach((font) => fonts.push(font.family))
}
if (navigator.plugins) {
for (let i = 0; i < navigator.plugins.length; i++) {
plugins.push(navigator.plugins[i].name)
}
}
return { fonts, plugins }
}
Combining all data points
async function getFingerprint() {
const canvasFingerprint = await getCanvasFingerprint()
const webGLInfo = getWebGLInfo()
const audioFingerprint = await getAudioFingerprint()
const fontsAndPlugins = getFontsAndPlugins()
return {
...browserInfo,
canvasFingerprint,
webGLInfo,
audioFingerprint,
...fontsAndPlugins,
}
}
This fingerprint can be hashed and stored in a database. When a user logs in, we can compare the new fingerprint with the stored one to identify if it's the same device.
async function hashFingerprintData(data) {
// sort for consistency
const sorted = Object.keys(data)
.sort()
.reduce((obj, key) => {
obj[key] = data[key]
return obj
}, {})
const json = JSON.stringify(sorted)
const encoder = new TextEncoder()
const encoded = encoder.encode(json)
const hashBuffer = await crypto.subtle.digest('SHA-256', encoded)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
The block below shows the demo of the fingerprinting process running on this page. You can try to open this page in different browsers and devices, or incognito mode, to see how the fingerprint changes. The block below is an iframe and you can use browser developer tools to view the iframe source code and see how the fingerprint is generated.
Privacy Note: This demo runs entirely in your browser. No data is stored or sent anywhere. The fingerprint you see is calculated locally using your device's browser info.
There are third-party libraries that can help you with browser fingerprinting. Two of the most common ones I've come across are FingerprintJS and Fingerprinting API.
Have you ever used browser fingerprinting in your projects? How did you handle privacy concerns and user consent?