Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/GleapSession.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export default class GleapSession {
// never re-identified from stale state.
lastIdentify = null;
identifyInFlight = false;
// `<gleapId>:<userId>` once an identify with a userHash for that user landed on
// that session (memory, plus the cache unless localStorage is disabled).
// An unchanged identify is skipped, but a session identified without a hash
// before (an older app version, identity verification switched on later, a
// sign-up identified before it could get one) still has to send the hash
// once: only that request lets the server mark the contact as proved.
// Keyed by user, never by the hash itself, so no credential is stored.
signedIdentity = null;
// identify / updateContact requests still waiting for the server. A survey
// started meanwhile would get the previous contact (Surveys 2.0 skips the
// contact questions an identified contact already answered), so showSurvey
Expand Down Expand Up @@ -248,6 +256,7 @@ export default class GleapSession {
// replay state so the next session can't inherit a stale identity.
this.lastIdentify = null;
this.identifyInFlight = false;
this.forgetSignedIdentity();
this.contactVersion++;

GleapFrameManager.getInstance().sendMessage(
Expand Down Expand Up @@ -643,14 +652,50 @@ export default class GleapSession {
}));
};

signedIdentityKey = () => `signed-identify-${this.sdkKey}`;

signedIdentityOf = (gleapId, userId) => `${gleapId}:${userId}`;

rememberSignedIdentity = (sessionData, userId) => {
if (!sessionData || !sessionData.gleapId || userId === undefined || userId === null) {
return;
}
this.signedIdentity = this.signedIdentityOf(sessionData.gleapId, userId);
if (!this.localStorageDisabled) {
try {
saveToGleapCache(this.signedIdentityKey(), this.signedIdentity);
} catch (exp) {}
}
};

forgetSignedIdentity = () => {
this.signedIdentity = null;
try {
saveToGleapCache(this.signedIdentityKey(), null);
} catch (exp) {}
};

/** A userHash this session has not been identified with yet for this user. */
needsSignedIdentify = (userId, userHash) => {
if (!userHash || userId === undefined || userId === null || !this.session || !this.session.gleapId) {
return false;
}
if (!this.signedIdentity && !this.localStorageDisabled) {
try {
this.signedIdentity = loadFromGleapCache(this.signedIdentityKey());
} catch (exp) {}
}
return this.signedIdentity !== this.signedIdentityOf(this.session.gleapId, userId);
};

identifySession = (userId, userData, userHash, refreshFileSession = false) => {
// Remember the args before any early-out: a no-op identify against a
// session that later gets recreated must still be replayable.
this.lastIdentify = { userId, userData, userHash };

const sessionNeedsUpdate = this.checkIfSessionNeedsUpdate(userId, userData);
const needsFileIdentity = !!userHash && this.session?.authenticatedFilesRequired && (!this.session?.fileAccessToken || Date.parse(this.session.fileAccessExpiresAt) <= Date.now());
if (!sessionNeedsUpdate && !refreshFileSession && !needsFileIdentity) {
if (!sessionNeedsUpdate && !refreshFileSession && !needsFileIdentity && !this.needsSignedIdentify(userId, userHash)) {
return;
}

Expand Down Expand Up @@ -684,6 +729,9 @@ export default class GleapSession {
try {
const sessionData = JSON.parse(http.responseText);
self.contactVersion++;
if (userHash) {
self.rememberSignedIdentity(sessionData, userId);
}
self.validateSession(sessionData);
resolve(sessionData);
} catch (exp) {
Expand Down
59 changes: 59 additions & 0 deletions src/GleapSession.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,65 @@ describe('identify replay', () => {
});
});

describe('identify with a userHash on a session identified without one', () => {
const USER_DATA = { email: 'bec@example.com', name: 'Bec' };
const respondIdentified = (xhr) => xhr.respond(200, JSON.stringify({ ...CACHED_IDENTIFIED_SESSION }));

it('sends the hashed identify once, although the cached session already has this user and data', () => {
seedCachedSession();
const session = buildSession();
session.startSession();

session.identifySession('user-1', USER_DATA, 'verified-hmac');

expect(identifyRequests()).toHaveLength(1);
expect(JSON.parse(identifyRequests()[0].body).userHash).toBe('verified-hmac');
respondIdentified(identifyRequests()[0]);

// The next page load: same cached session, same identify, nothing to prove again.
const nextBoot = buildSession();
nextBoot.startSession();
nextBoot.identifySession('user-1', USER_DATA, 'verified-hmac');
expect(identifyRequests()).toHaveLength(1);
});

it('sends it again when the hashed identify failed', () => {
seedCachedSession();
const session = buildSession();
session.startSession();
session.identifySession('user-1', USER_DATA, 'verified-hmac')?.catch?.(() => {});
identifyRequests()[0].respond(503);

const nextBoot = buildSession();
nextBoot.startSession();
nextBoot.identifySession('user-1', USER_DATA, 'verified-hmac');
expect(identifyRequests()).toHaveLength(2);
});

it('still skips an unchanged identify without a hash', () => {
seedCachedSession();
const session = buildSession();
session.startSession();
session.identifySession('user-1', USER_DATA);
expect(identifyRequests()).toHaveLength(0);
});

it('forgets the proof on logout', () => {
seedCachedSession();
const session = buildSession();
session.startSession();
session.identifySession('user-1', USER_DATA, 'verified-hmac');
respondIdentified(identifyRequests()[0]);

session.clearSession(0, false);
seedCachedSession();
const nextBoot = buildSession();
nextBoot.startSession();
nextBoot.identifySession('user-1', USER_DATA, 'verified-hmac');
expect(identifyRequests()).toHaveLength(2);
});
});

describe('user switch in the same browser', () => {
const OTHER_USER_SESSION = {
gleapId: 'gleap-id-b',
Expand Down
Loading