mirror of
https://github.com/HeyPuter/puter.git
synced 2026-07-08 08:12:15 +00:00
feat: fingerprinting for signup checks (#3254)
This commit is contained in:
Generated
+7
-12
@@ -1513,18 +1513,6 @@
|
||||
"sisteransi": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/kv-asset-handler": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz",
|
||||
@@ -6652,6 +6640,12 @@
|
||||
"eslint": "^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@thumbmarkjs/thumbmarkjs": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@thumbmarkjs/thumbmarkjs/-/thumbmarkjs-1.9.1.tgz",
|
||||
"integrity": "sha512-Y8dhwoi53uS+dNUy9KGWgLftjY/zZVmenv0+cOZ00z1GKMagAdPmbG8lDMcrwMvYngEPq+7Vr9TQeuy3tmDrjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tokenizer/inflate": {
|
||||
"version": "0.4.1",
|
||||
"license": "MIT",
|
||||
@@ -18561,6 +18555,7 @@
|
||||
"src/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@thumbmarkjs/thumbmarkjs": "1.9.1",
|
||||
"file-type": "21.3.3",
|
||||
"json-colorizer": "^3.0.1",
|
||||
"music-metadata": "11.12.3",
|
||||
|
||||
@@ -131,6 +131,9 @@ export type EventMap = {
|
||||
data?: unknown;
|
||||
abuse?: unknown;
|
||||
trail?: Array<string>;
|
||||
/** Device signals forwarded verbatim from the signup request body. */
|
||||
fingerprint?: string | null;
|
||||
dfp_telemetry_id?: string | null;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
'puter.signup.success': {
|
||||
@@ -139,6 +142,9 @@ export type EventMap = {
|
||||
email: string;
|
||||
username: string;
|
||||
ip?: string | null;
|
||||
fingerprint?: string | null;
|
||||
/** True when the created account is a temp user (no email/password). */
|
||||
is_temp?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
'email.validate': {
|
||||
|
||||
@@ -523,6 +523,247 @@ describe('AuthController.handleSignup', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -- Signup device signals (fingerprint + dfp_telemetry_id) --
|
||||
|
||||
describe('AuthController.handleSignup device signals', () => {
|
||||
const uniq = () => Math.random().toString(36).slice(2, 10);
|
||||
|
||||
const captureValidateEvents = async (
|
||||
fn: () => Promise<void>,
|
||||
): Promise<Array<Record<string, unknown>>> => {
|
||||
const seen: Array<Record<string, unknown>> = [];
|
||||
await withSignupValidateOverride((event) => {
|
||||
seen.push(event as unknown as Record<string, unknown>);
|
||||
}, fn);
|
||||
return seen;
|
||||
};
|
||||
|
||||
const successEventsFor = (baseline: number, username: string) =>
|
||||
heardSignupSuccess
|
||||
.slice(baseline)
|
||||
.filter(
|
||||
(evt) => (evt as { username?: string }).username === username,
|
||||
);
|
||||
|
||||
it('forwards fingerprint and dfp_telemetry_id verbatim to validate and success events', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
const fingerprint = 'Fp_abc.123-XYZ';
|
||||
const dfpTelemetryId = 'tel_id-456';
|
||||
|
||||
const seen = await captureValidateEvents(async () => {
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint,
|
||||
dfp_telemetry_id: dfpTelemetryId,
|
||||
}),
|
||||
res,
|
||||
);
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
});
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0].fingerprint).toBe(fingerprint);
|
||||
expect(seen[0].dfp_telemetry_id).toBe(dfpTelemetryId);
|
||||
|
||||
const successes = successEventsFor(baseline, username);
|
||||
expect(successes).toHaveLength(1);
|
||||
expect(successes[0].fingerprint).toBe(fingerprint);
|
||||
expect(successes[0].is_temp).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts boundary-length values (128-char fingerprint, 64-char dfp_telemetry_id)', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint: 'f'.repeat(128),
|
||||
dfp_telemetry_id: 'd'.repeat(64),
|
||||
}),
|
||||
res,
|
||||
);
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults both fields to null on the validate event when absent', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
|
||||
const seen = await captureValidateEvents(async () => {
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
}),
|
||||
res,
|
||||
);
|
||||
// Signup completes exactly as before when the fields are absent.
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
expect(res.cookies['puter_auth_token']).toBeDefined();
|
||||
});
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0].fingerprint).toBeNull();
|
||||
expect(seen[0].dfp_telemetry_id).toBeNull();
|
||||
|
||||
const successes = successEventsFor(baseline, username);
|
||||
expect(successes).toHaveLength(1);
|
||||
expect(successes[0].fingerprint).toBeNull();
|
||||
expect(successes[0].is_temp).toBe(false);
|
||||
});
|
||||
|
||||
it('treats empty-string fingerprint and dfp_telemetry_id as absent', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
|
||||
const seen = await captureValidateEvents(async () => {
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint: '',
|
||||
dfp_telemetry_id: '',
|
||||
}),
|
||||
res,
|
||||
);
|
||||
// An empty signal is "not collected", never a 400.
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
});
|
||||
|
||||
expect(seen).toHaveLength(1);
|
||||
expect(seen[0].fingerprint).toBeNull();
|
||||
expect(seen[0].dfp_telemetry_id).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects a non-string fingerprint with 400 and fires no success event', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint: 12345,
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
statusCode: 400,
|
||||
legacyCode: 'bad_request',
|
||||
});
|
||||
expect(heardSignupSuccess.length).toBe(baseline);
|
||||
// No user row was created either.
|
||||
expect(await server.stores.user.getByUsername(username)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('rejects a fingerprint longer than 128 characters with 400 and fires no success event', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint: 'f'.repeat(129),
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
expect(heardSignupSuccess.length).toBe(baseline);
|
||||
});
|
||||
|
||||
it('rejects a dfp_telemetry_id longer than 64 characters with 400 and fires no success event', async () => {
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email: `${username}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
dfp_telemetry_id: 'd'.repeat(65),
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
expect(heardSignupSuccess.length).toBe(baseline);
|
||||
});
|
||||
|
||||
it('rejects a non-string dfp_telemetry_id with 400', async () => {
|
||||
await expect(
|
||||
controller.handleSignup(
|
||||
makeReq({
|
||||
username: `fp_${uniq()}`,
|
||||
email: `${uniq()}@test.local`,
|
||||
password: 'correct-horse-battery',
|
||||
dfp_telemetry_id: { nested: true },
|
||||
}),
|
||||
makeRes(),
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
});
|
||||
|
||||
it('temp-user signup reports is_temp true and carries the fingerprint on the success event', async () => {
|
||||
const baseline = heardSignupSuccess.length;
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({ is_temp: true, fingerprint: 'temp-device-fp' }),
|
||||
res,
|
||||
);
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
const username = (res.body as { user: { username: string } }).user
|
||||
.username;
|
||||
const successes = successEventsFor(baseline, username);
|
||||
expect(successes).toHaveLength(1);
|
||||
expect(successes[0].is_temp).toBe(true);
|
||||
expect(successes[0].fingerprint).toBe('temp-device-fp');
|
||||
});
|
||||
|
||||
it('pseudo-user claim reports is_temp false on the success event', async () => {
|
||||
// Seed an unconfirmed placeholder row (email set, password null) —
|
||||
// signing up with the same email claims it instead of inserting.
|
||||
const placeholder = `ph_${uniq()}`;
|
||||
const email = `${placeholder}@test.local`;
|
||||
await server.stores.user.create({
|
||||
username: placeholder,
|
||||
uuid: uuidv4(),
|
||||
password: null,
|
||||
email,
|
||||
});
|
||||
|
||||
const username = `fp_${uniq()}`;
|
||||
const baseline = heardSignupSuccess.length;
|
||||
const res = makeRes();
|
||||
await controller.handleSignup(
|
||||
makeReq({
|
||||
username,
|
||||
email,
|
||||
password: 'correct-horse-battery',
|
||||
fingerprint: 'claim-device-fp',
|
||||
}),
|
||||
res,
|
||||
);
|
||||
expect(isCompleteLoginResponse(res.body)).toBe(true);
|
||||
|
||||
const successes = successEventsFor(baseline, username);
|
||||
expect(successes).toHaveLength(1);
|
||||
expect(successes[0].is_temp).toBe(false);
|
||||
expect(successes[0].fingerprint).toBe('claim-device-fp');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Login flow ──────────────────────────────────────────────────────
|
||||
|
||||
describe('AuthController.handleLogin', () => {
|
||||
|
||||
@@ -57,6 +57,8 @@ import { PuterController } from '../types.js';
|
||||
|
||||
const USERNAME_REGEX = /^\w{1,}$/;
|
||||
const USERNAME_MAX_LENGTH = 45;
|
||||
const FINGERPRINT_MAX_LENGTH = 128;
|
||||
const DFP_TELEMETRY_ID_MAX_LENGTH = 64;
|
||||
const RESERVED_USERNAMES = new Set([
|
||||
'admin',
|
||||
'administrator',
|
||||
@@ -375,6 +377,43 @@ export class AuthController extends PuterController {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optional device signals (browser fingerprint hash, DFP telemetry
|
||||
// id). Core only enforces shape and forwards the values verbatim —
|
||||
// signup-abuse policy built on them lives in extensions. Checked
|
||||
// before the reauth short-circuit so a malformed value is rejected
|
||||
// on every /signup path.
|
||||
if (body.fingerprint !== undefined && body.fingerprint !== null) {
|
||||
if (typeof body.fingerprint !== 'string')
|
||||
throw new HttpError(400, 'fingerprint must be a string.', {
|
||||
legacyCode: 'bad_request',
|
||||
});
|
||||
if (body.fingerprint.length > FINGERPRINT_MAX_LENGTH)
|
||||
throw new HttpError(
|
||||
400,
|
||||
`fingerprint cannot be longer than ${FINGERPRINT_MAX_LENGTH} characters.`,
|
||||
{ legacyCode: 'bad_request' },
|
||||
);
|
||||
}
|
||||
if (
|
||||
body.dfp_telemetry_id !== undefined &&
|
||||
body.dfp_telemetry_id !== null
|
||||
) {
|
||||
if (typeof body.dfp_telemetry_id !== 'string')
|
||||
throw new HttpError(400, 'dfp_telemetry_id must be a string.', {
|
||||
legacyCode: 'bad_request',
|
||||
});
|
||||
if (body.dfp_telemetry_id.length > DFP_TELEMETRY_ID_MAX_LENGTH)
|
||||
throw new HttpError(
|
||||
400,
|
||||
`dfp_telemetry_id cannot be longer than ${DFP_TELEMETRY_ID_MAX_LENGTH} characters.`,
|
||||
{ legacyCode: 'bad_request' },
|
||||
);
|
||||
}
|
||||
// Empty strings are treated as absent — a signal that wasn't
|
||||
// collected, not a malformed request.
|
||||
const fingerprint: string | null = body.fingerprint || null;
|
||||
const dfp_telemetry_id: string | null = body.dfp_telemetry_id || null;
|
||||
|
||||
// Temp-user reauth short-circuit: when an existing temp user is
|
||||
// forced through the reauth flow, the GUI re-submits /signup with
|
||||
// is_temp=true plus the server-signed reauth_token from the 401.
|
||||
@@ -557,6 +596,8 @@ export class AuthController extends PuterController {
|
||||
message: null,
|
||||
code: null,
|
||||
user_agent: req?.headers?.['user-agent'] ?? null,
|
||||
fingerprint,
|
||||
dfp_telemetry_id,
|
||||
// Populated by the abuse extension's v2 harness; persisted to the
|
||||
// user row below so the signup-time reputation is referable later.
|
||||
reputation: null as number | null,
|
||||
@@ -678,6 +719,8 @@ export class AuthController extends PuterController {
|
||||
ip_fwd: proxyIpChain,
|
||||
user_agent: req.headers?.['user-agent'],
|
||||
origin: req.headers?.origin,
|
||||
fingerprint,
|
||||
dfp_telemetry_id,
|
||||
},
|
||||
signup_ip: clientIp,
|
||||
signup_ip_forwarded: proxyIpChain,
|
||||
@@ -757,6 +800,11 @@ export class AuthController extends PuterController {
|
||||
user_uuid: user!.uuid,
|
||||
email: user!.email,
|
||||
username: user!.username,
|
||||
fingerprint,
|
||||
// Reflects the row that was actually created/claimed —
|
||||
// a pseudo-user claim ends up with credentials, so it
|
||||
// reports false here. Same signal completeLogin uses.
|
||||
is_temp: user!.password === null && user!.email === null,
|
||||
ip:
|
||||
(req?.headers?.['x-forwarded-for'] as
|
||||
| string
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@thumbmarkjs/thumbmarkjs": "1.9.1",
|
||||
"file-type": "21.3.3",
|
||||
"json-colorizer": "^3.0.1",
|
||||
"music-metadata": "11.12.3",
|
||||
|
||||
@@ -269,7 +269,7 @@ function UIWindowSignup (options) {
|
||||
}
|
||||
});
|
||||
|
||||
$(el_window).find('.signup-btn').on('click', function (e) {
|
||||
$(el_window).find('.signup-btn').on('click', async function (e) {
|
||||
// Clear previous error states
|
||||
$(el_window).find('.signup-error-msg').hide();
|
||||
|
||||
@@ -351,6 +351,18 @@ function UIWindowSignup (options) {
|
||||
headers = window.custom_headers;
|
||||
}
|
||||
|
||||
// Device signals for abuse prevention; omitted when unavailable
|
||||
let fingerprint = null;
|
||||
let dfpTelemetryId = null;
|
||||
try {
|
||||
[fingerprint, dfpTelemetryId] = await Promise.all([
|
||||
window.getDeviceFingerprint?.(),
|
||||
window.getDfpTelemetryId?.(),
|
||||
]);
|
||||
} catch (_) {
|
||||
// signup must never block or fail because of device signals
|
||||
}
|
||||
|
||||
// Include captcha in request only if required
|
||||
const requestData = {
|
||||
username: username,
|
||||
@@ -361,6 +373,12 @@ function UIWindowSignup (options) {
|
||||
p102xyzname: p102xyzname,
|
||||
'cf-turnstile-response': turnstileToken,
|
||||
};
|
||||
if ( fingerprint ) {
|
||||
requestData.fingerprint = fingerprint;
|
||||
}
|
||||
if ( dfpTelemetryId ) {
|
||||
requestData.dfp_telemetry_id = dfpTelemetryId;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `${window.gui_origin }/signup`,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
* This file is part of Puter.
|
||||
*
|
||||
* Puter is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Thumbmark } from '@thumbmarkjs/thumbmarkjs';
|
||||
|
||||
const FINGERPRINT_TIMEOUT = 1500;
|
||||
const DFP_TELEMETRY_TIMEOUT = 2500;
|
||||
// Server-side caps for the matching /signup fields; values that would be
|
||||
// rejected there are dropped client-side so signup can never 400 over them.
|
||||
const FINGERPRINT_MAX_LENGTH = 128;
|
||||
const DFP_TELEMETRY_ID_MAX_LENGTH = 64;
|
||||
|
||||
// Resolves null on rejection or timeout so signup flows can await these
|
||||
// signals unconditionally without ever blocking or failing on them.
|
||||
const settleWithin = (promise, ms) => {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => resolve(null), ms);
|
||||
promise.then(
|
||||
(value) => { clearTimeout(timer); resolve(value ?? null); },
|
||||
() => { clearTimeout(timer); resolve(null); },
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const asPlausible = (value, maxLength) => {
|
||||
return typeof value === 'string' && value.length > 0 && value.length <= maxLength
|
||||
? value
|
||||
: null;
|
||||
};
|
||||
|
||||
let fingerprint_promise = null;
|
||||
const computeFingerprint = () => {
|
||||
if ( ! fingerprint_promise ) {
|
||||
// logging:false keeps the library fully offline; it would otherwise
|
||||
// send a small sample of fingerprints to thumbmarkjs.com.
|
||||
fingerprint_promise = new Thumbmark({ logging: false, timeout: FINGERPRINT_TIMEOUT })
|
||||
.get()
|
||||
.then(result => result?.thumbmark || null);
|
||||
fingerprint_promise.catch(error => {
|
||||
// Don't cache the failure — a later attempt may succeed.
|
||||
fingerprint_promise = null;
|
||||
console.debug('device fingerprint unavailable:', error);
|
||||
});
|
||||
}
|
||||
return fingerprint_promise;
|
||||
};
|
||||
|
||||
let stytch_script_promise = null;
|
||||
const loadStytchScript = () => {
|
||||
if ( ! stytch_script_promise ) {
|
||||
stytch_script_promise = window.loadScript('https://elements.stytch.com/telemetry.js');
|
||||
stytch_script_promise.catch(error => {
|
||||
// Don't cache the failure — a transient load error would
|
||||
// otherwise disable DFP for the rest of the session.
|
||||
stytch_script_promise = null;
|
||||
console.debug('Stytch telemetry script unavailable:', error);
|
||||
});
|
||||
}
|
||||
return stytch_script_promise;
|
||||
};
|
||||
|
||||
// Telemetry ids are short-lived, so unlike the fingerprint this is fetched
|
||||
// fresh on every call; only the script load itself is reused.
|
||||
const fetchDfpTelemetryId = async () => {
|
||||
await loadStytchScript();
|
||||
if ( typeof window.GetTelemetryID !== 'function' ) {
|
||||
return null;
|
||||
}
|
||||
const telemetry_id = await window.GetTelemetryID({
|
||||
publicToken: window.gui_params.stytchPublicToken,
|
||||
});
|
||||
return telemetry_id || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs window.getDeviceFingerprint() and window.getDfpTelemetryId(). Both
|
||||
* getters resolve to string|null and never reject. Collection is lazy: no
|
||||
* probing happens and no third-party script is loaded until a signup flow
|
||||
* actually asks. The fingerprint needs no credentials so it's on by default
|
||||
* (gui_params.thumbmarkEnabled = false is the kill switch); the Stytch
|
||||
* telemetry id requires a public token and is collected only when one is
|
||||
* configured.
|
||||
*/
|
||||
const init_device_signals = () => {
|
||||
window.getDeviceFingerprint = () => {
|
||||
try {
|
||||
if ( window.gui_params?.thumbmarkEnabled === false ) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return settleWithin(computeFingerprint(), FINGERPRINT_TIMEOUT)
|
||||
.then(value => asPlausible(value, FINGERPRINT_MAX_LENGTH));
|
||||
} catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.getDfpTelemetryId = () => {
|
||||
try {
|
||||
if ( ! window.gui_params?.stytchPublicToken ) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return settleWithin(fetchDfpTelemetryId(), DFP_TELEMETRY_TIMEOUT)
|
||||
.then(value => asPlausible(value, DFP_TELEMETRY_ID_MAX_LENGTH));
|
||||
} catch (e) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default init_device_signals;
|
||||
+23
-1
@@ -36,6 +36,7 @@ import UIWindowSignup from './UI/UIWindowSignup.js';
|
||||
import UIWindowRecoverPassword from './UI/UIWindowRecoverPassword.js';
|
||||
import { PROCESS_RUNNING } from './definitions.js';
|
||||
import create_access_token from './helpers/create_access_token.js';
|
||||
import init_device_signals from './helpers/device_signals.js';
|
||||
import item_icon from './helpers/item_icon.js';
|
||||
import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js';
|
||||
import update_mouse_position from './helpers/update_mouse_position.js';
|
||||
@@ -302,6 +303,11 @@ window.initgui = async function (options) {
|
||||
const url_paths = window.location.pathname.split('/').filter(element => element);
|
||||
window.url_paths = url_paths;
|
||||
|
||||
// Install device signal helpers; collection is lazy. The fingerprint is
|
||||
// on by default (gui_params.thumbmarkEnabled = false kills it); the Stytch
|
||||
// telemetry id needs gui_params.stytchPublicToken.
|
||||
init_device_signals();
|
||||
|
||||
let picked_a_user_for_sdk_login = false;
|
||||
|
||||
// update SDK if auth_token is different from the one in the SDK
|
||||
@@ -1238,7 +1244,7 @@ window.initgui = async function (options) {
|
||||
}
|
||||
|
||||
// Function to create temp user after captcha completion
|
||||
const createTempUser = (turnstileToken) => {
|
||||
const createTempUser = async (turnstileToken) => {
|
||||
// if this is a popup, show a spinner
|
||||
let spinner_init_ts = Date.now();
|
||||
const requestData = {
|
||||
@@ -1251,6 +1257,22 @@ window.initgui = async function (options) {
|
||||
requestData['cf-turnstile-response'] = turnstileToken;
|
||||
}
|
||||
|
||||
// Device signals for abuse prevention; omitted when unavailable
|
||||
try {
|
||||
const [fingerprint, dfpTelemetryId] = await Promise.all([
|
||||
window.getDeviceFingerprint?.(),
|
||||
window.getDfpTelemetryId?.(),
|
||||
]);
|
||||
if ( fingerprint ) {
|
||||
requestData.fingerprint = fingerprint;
|
||||
}
|
||||
if ( dfpTelemetryId ) {
|
||||
requestData.dfp_telemetry_id = dfpTelemetryId;
|
||||
}
|
||||
} catch (e) {
|
||||
// signup must never block or fail because of device signals
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `${window.gui_origin }/signup`,
|
||||
type: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user