• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kiva / ui / 19147367510

06 Nov 2025 07:26PM UTC coverage: 91.443% (+41.5%) from 49.902%
19147367510

push

github

emuvente
test: refactor category-row-arrows-visible-mixin test with runner method

3722 of 3979 branches covered (93.54%)

Branch coverage included in aggregate %.

18923 of 20785 relevant lines covered (91.04%)

78.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.28
/src/util/KvAuth0.js
1
import { sub } from 'date-fns';
1✔
2
import * as Sentry from '@sentry/vue'; // could be async import
1✔
3
import syncDate from './syncDate';
1✔
4
import logFormatter from './logFormatter';
1✔
5

6
export const KIVA_ID_KEY = 'https://www.kiva.org/kiva_id';
1✔
7
export const LAST_LOGIN_KEY = 'https://www.kiva.org/last_login';
1✔
8
export const USED_MFA_KEY = 'https://www.kiva.org/used_mfa';
1✔
9
const FAKE_AUTH_NAME = 'kvfa';
1✔
10
const ALLOWED_FAKE_AUTH_DOMAINS = [
1✔
11
        'login.dev.kiva.org',
1✔
12
        'login.stage.kiva.org',
1✔
13
];
1✔
14
const SYNC_NAME = 'kvls';
1✔
15
const LOGOUT_VALUE = 'o';
1✔
16
const COOKIE_OPTIONS = { path: '/', secure: true };
1✔
17

18
// These symbols are unique, and therefore are private to this scope.
1✔
19
// For more details, see https://medium.com/@davidrhyswhite/private-members-in-es6-db1ccd6128a5
1✔
20
const initWebAuth = Symbol('initWebAuth');
1✔
21
const errorCallbacks = Symbol('errorCallbacks');
1✔
22
const handleUnknownError = Symbol('handleUnknownError');
1✔
23
const mfaTokenPromise = Symbol('mfaTokenPromise');
1✔
24
const sessionPromise = Symbol('sessionPromise');
1✔
25
const setAuthData = Symbol('setAuthData');
1✔
26
const setMfaAuthData = Symbol('setMfaAuthData');
1✔
27
const parseMfaHash = Symbol('parseMfaHash');
1✔
28
const noteLoggedIn = Symbol('noteLoggedIn');
1✔
29
const noteLoggedOut = Symbol('noteLoggedOut');
1✔
30
const clearNotedLoginState = Symbol('clearNotedLoginState');
1✔
31

32
function getErrorString(err) {
×
33
        return `${err.error || err.code || err.name}: ${err.error_description || err.description}`;
×
34
}
×
35

36
function isAuth0Hash(hash) {
×
37
        if (hash.indexOf('error') === -1
×
38
                && hash.indexOf('access_token') === -1
×
39
                && hash.indexOf('id_token') === -1
×
40
                && hash.indexOf('refresh_token') === -1) {
×
41
                return false;
×
42
        }
×
43
        return true;
×
44
}
×
45

46
async function storeRedirectState() {
×
47
        const { default: store2 } = await import('store2');
×
48

49
        const { pathname, search } = window.location;
×
50
        store2.session('auth0.redirect', `${pathname}${search}`);
×
51

52
        const state = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
×
53
        store2.session('auth0.state', state);
×
54

55
        return state;
×
56
}
×
57

58
// Class to handle interacting with auth0 in the browser
1✔
59
export default class KvAuth0 {
1✔
60
        constructor({
1✔
61
                accessToken = '',
55✔
62
                audience,
55✔
63
                checkFakeAuth = false,
55✔
64
                clientID,
55✔
65
                cookieStore,
55✔
66
                domain,
55✔
67
                mfaAudience,
55✔
68
                redirectUri,
55✔
69
                scope,
55✔
70
                user = null,
55✔
71
        }) {
55✔
72
                this[errorCallbacks] = [];
55✔
73
                this.enabled = true;
55✔
74
                this.user = user;
55✔
75
                this.accessToken = accessToken;
55✔
76
                this.isServer = typeof window === 'undefined';
55✔
77
                this.checkFakeAuth = !!checkFakeAuth;
55✔
78
                this.cookieStore = cookieStore;
55✔
79

80
                // properties for WebAuth clients
55✔
81
                this.audience = audience;
55✔
82
                this.mfaAudience = mfaAudience;
55✔
83
                this.clientID = clientID;
55✔
84
                this.domain = domain;
55✔
85
                this.redirectUri = redirectUri;
55✔
86
                this.scope = scope;
55✔
87

88
                if (this.fakeAuthAllowed()) {
55✔
89
                        // Set user from fake auth cookie if available
25✔
90
                        const idTokenPayload = this.getFakeIdTokenPayload();
25✔
91
                        if (idTokenPayload) {
25✔
92
                                this[setAuthData]({ idTokenPayload });
16✔
93
                                this[noteLoggedIn]();
16✔
94
                        }
16✔
95
                }
25✔
96
        }
55✔
97

98
        // Setup Auth0 WebAuth client for authentication
1✔
99
        [initWebAuth]() {
1✔
100
                if (this.webAuth || this.isServer) {
×
101
                        return Promise.resolve();
×
102
                }
×
103
                return import('auth0-js').then(({ default: auth0js }) => {
×
104
                        this.webAuth = new auth0js.WebAuth({
×
105
                                clientID: this.clientID,
×
106
                                domain: this.domain,
×
107
                                redirectUri: this.redirectUri,
×
108
                        });
×
109
                });
×
110
        }
×
111

112
        [setAuthData]({ idTokenPayload, accessToken, expiresIn } = {}) {
1✔
113
                this.user = idTokenPayload || null;
16!
114
                this.accessToken = accessToken || '';
16✔
115

116
                if (expiresIn > 0) {
16!
117
                        setTimeout(() => {
×
118
                                // set null auth data to remove expired token
×
119
                                this[setAuthData]();
×
120
                        }, Number(expiresIn) * 1000);
×
121
                }
×
122
        }
16✔
123

124
        [setMfaAuthData]({ accessToken, expiresIn } = {}) {
1✔
125
                // save access token as this.mfaManagementToken
×
126
                this.mfaManagementToken = accessToken;
×
127
                // mfa management token expiration handling
×
128
                if (expiresIn > 0) {
×
129
                        setTimeout(() => {
×
130
                                this.mfaManagementToken = '';
×
131
                        }, Number(expiresIn) * 1000);
×
132
                }
×
133
        }
×
134

135
        /* eslint-disable no-underscore-dangle */
1✔
136

137
        // Return the kiva id for the current user (or undefined)
1✔
138
        getKivaId() {
1✔
139
                return this.user?.[KIVA_ID_KEY] || this.user?._json?.[KIVA_ID_KEY];
22✔
140
        }
22✔
141

142
        // Return the last login timestamp for the current user (or 0)
1✔
143
        getLastLogin() {
1✔
144
                return this.user?.[LAST_LOGIN_KEY] || this.user?._json?.[LAST_LOGIN_KEY] || 0;
3✔
145
        }
3✔
146

147
        isMfaAuthenticated() {
1✔
148
                return this.user?.[USED_MFA_KEY] || this.user?._json?.[USED_MFA_KEY] || false;
5✔
149
        }
5✔
150

151
        /* eslint-enable no-underscore-dangle */
1✔
152

153
        // Return true iff fake auth should be checked and fake auth is allowed for the current domain
1✔
154
        fakeAuthAllowed() {
1✔
155
                return this.checkFakeAuth && ALLOWED_FAKE_AUTH_DOMAINS.includes(this.domain);
106✔
156
        }
106✔
157

158
        getFakeAuthCookieValue() {
1✔
159
                if (!this.fakeAuthAllowed()) return;
46✔
160

161
                const cookieValue = this.cookieStore.get(FAKE_AUTH_NAME) ?? '';
46✔
162

163
                const cookieParts = cookieValue.split(':');
46✔
164
                const userId = parseInt(cookieParts[0], 10);
46✔
165

166
                if (userId) {
46✔
167
                        const scopes = (cookieParts[1] ?? '').split('/').filter(x => x);
28✔
168

169
                        return { userId, scopes };
28✔
170
                }
28✔
171
        }
46✔
172

173
        getFakeIdTokenPayload() {
1✔
174
                const { userId, scopes } = this.getFakeAuthCookieValue() ?? {};
35✔
175

176
                if (userId) {
35✔
177
                        try {
22✔
178
                                let lastLogin;
22✔
179
                                const now = new Date();
22✔
180
                                if (scopes.includes('recent')) {
22✔
181
                                        // current time
7✔
182
                                        lastLogin = now.getTime();
7✔
183
                                } else if (scopes.includes('active')) {
22✔
184
                                        // current time - 5:01
3✔
185
                                        lastLogin = sub(now, { minutes: 5, seconds: 1 }).getTime();
3✔
186
                                } else {
15✔
187
                                        // current time - 1:00:01
12✔
188
                                        lastLogin = sub(now, { hours: 1, seconds: 1 }).getTime();
12✔
189
                                }
12✔
190

191
                                return {
22✔
192
                                        [KIVA_ID_KEY]: userId,
22✔
193
                                        [LAST_LOGIN_KEY]: lastLogin,
22✔
194
                                        [USED_MFA_KEY]: scopes.includes('mfa'),
22✔
195
                                        scopes,
22✔
196
                                };
22✔
197
                        } catch (e) {
22!
198
                                console.error(e);
×
199
                                Sentry.captureException(e);
×
200
                        }
×
201
                }
22✔
202
        }
35✔
203

204
        getSyncCookieValue() {
1✔
205
                return this.cookieStore.get(SYNC_NAME);
10✔
206
        }
10✔
207

208
        isNotedLoggedIn() {
1✔
209
                const syncValue = this.getSyncCookieValue();
4✔
210
                return syncValue && syncValue !== LOGOUT_VALUE;
4✔
211
        }
4✔
212

213
        isNotedLoggedOut() {
1✔
214
                return this.getSyncCookieValue() === LOGOUT_VALUE;
2✔
215
        }
2✔
216

217
        isNotedUserSessionUser() {
1✔
218
                return String(this.getKivaId()) === String(this.getSyncCookieValue());
3✔
219
        }
3✔
220

221
        [noteLoggedIn]() {
1✔
222
                this.cookieStore.set(SYNC_NAME, this.getKivaId(), COOKIE_OPTIONS);
16✔
223
        }
16✔
224

225
        [noteLoggedOut]() {
1✔
226
                this.cookieStore.set(SYNC_NAME, LOGOUT_VALUE, COOKIE_OPTIONS);
×
227
        }
×
228

229
        [clearNotedLoginState]() {
1✔
230
                this.cookieStore.remove(SYNC_NAME, COOKIE_OPTIONS);
×
231
        }
×
232

233
        // Parse the hash from the URL to get the MFA management token
1✔
234
        [parseMfaHash]() {
1✔
235
                return new Promise((resolve, reject) => {
×
236
                        const { hash } = window.location;
×
237
                        if (isAuth0Hash(hash)) {
×
238
                                this.webAuth.parseHash({
×
239
                                        hash,
×
240
                                        responseType: 'token',
×
241
                                }, (err, result) => {
×
242
                                        if (err) {
×
243
                                                reject(err);
×
244
                                        } else {
×
245
                                                this[setMfaAuthData](result);
×
246
                                                resolve();
×
247
                                        }
×
248
                                });
×
249
                        } else {
×
250
                                resolve();
×
251
                        }
×
252
                });
×
253
        }
×
254

255
        // Silently fetch an access token for the MFA api to manage MFA factors
1✔
256
        getMfaManagementToken() {
1✔
257
                // only try this if in the browser
3✔
258
                if (this.isServer) {
3✔
259
                        return Promise.reject(new Error('getMfaManagementToken called in server mode'));
1✔
260
                }
1✔
261

262
                // Ensure only one mfa token request at a time
2✔
263
                if (this[mfaTokenPromise]) return this[mfaTokenPromise];
3!
264

265
                // Check for user before trying for token
2✔
266
                if (!this.user) {
3✔
267
                        return Promise.reject(new Error('api.authenticationRequired'));
1✔
268
                }
1✔
269

270
                // Resolve with management token if we already have one
1✔
271
                if (this.mfaManagementToken) {
1✔
272
                        return Promise.resolve(this.mfaManagementToken);
1✔
273
                }
1!
274

275
                // Fetch a new mfa management token
×
276
                this[mfaTokenPromise] = new Promise((resolve, reject) => {
×
277
                        // Initialize the web auth client
×
278
                        this[initWebAuth]()
×
279
                                // Parse the hash if it exists
×
280
                                .then(() => this[parseMfaHash]())
×
281
                                .then(() => {
×
282
                                        if (this.mfaManagementToken) {
×
283
                                                resolve(this.mfaManagementToken);
×
284
                                        } else {
×
285
                                                // If we still don't have a token, try to get one
×
286
                                                // Ensure browser clock is correct before fetching the token
×
287
                                                syncDate().then(() => {
×
288
                                                        // Store the current URL in session storage to redirect back to it after MFA
×
289
                                                        return storeRedirectState();
×
290
                                                }).then(state => {
×
291
                                                        this.webAuth.authorize({
×
292
                                                                state,
×
293
                                                                audience: this.mfaAudience,
×
294
                                                                responseType: 'token',
×
295
                                                                scope: 'enroll read:authenticators remove:authenticators',
×
296
                                                        });
×
297
                                                });
×
298
                                        }
×
299
                                })
×
300
                                .catch(reject);
×
301
                });
×
302

303
                // Once the promise completes, stop tracking it
×
304
                this[mfaTokenPromise].finally(() => {
×
305
                        this[mfaTokenPromise] = null;
×
306
                });
×
307

308
                return this[mfaTokenPromise];
×
309
        }
×
310

311
        // Silently check for a logged in session with auth0 using hidden iframes
1✔
312
        checkSession({ skipIfUserExists = false } = {}) {
1✔
313
                // only try this if in the browser
2✔
314
                if (this.isServer) {
2✔
315
                        return Promise.reject(new Error('checkSession called in server mode'));
1✔
316
                }
1✔
317

318
                if (skipIfUserExists && this.user) {
2✔
319
                        return Promise.resolve();
1✔
320
                }
1!
321

322
                // Ensure that we only check the session once at a time
×
323
                if (this[sessionPromise]) return this[sessionPromise];
×
324

325
                // Check for an existing session
×
326
                this[sessionPromise] = new Promise(resolve => {
×
327
                        // Ensure browser clock is correct before checking session
×
328
                        syncDate().then(() => this[initWebAuth]()).then(() => {
×
329
                                this.webAuth.checkSession({
×
330
                                        audience: this.audience,
×
331
                                        responseType: 'token id_token',
×
332
                                        scope: this.scope,
×
333
                                }, (err, result) => {
×
334
                                        if (err) {
×
335
                                                this[setAuthData]();
×
336
                                                if (err.error === 'login_required'
×
337
                                                        || err.error === 'unauthorized'
×
338
                                                        || err.error === 'access_denied'
×
339
                                                ) {
×
340
                                                        // User is not logged in, so continue without authentication
×
341
                                                        this[noteLoggedOut]();
×
342
                                                        resolve();
×
343
                                                } else if (err.error === 'consent_required' || err.error === 'interaction_required') {
×
344
                                                        // These errors require interaction beyond what can be provided by webauth,
×
345
                                                        // so resolve without authentication for now. Other possibility is to redirect
×
346
                                                        // to login to complete authentication.
×
347
                                                        Sentry.withScope(scope => {
×
348
                                                                scope.setLevel('warning');
×
349
                                                                Sentry.captureMessage(`Auth session not started: ${getErrorString(err)}`);
×
350
                                                        });
×
351
                                                        resolve();
×
352
                                                } else {
×
353
                                                        // Everything else, actually throw an error
×
354
                                                        Sentry.withScope(scope => {
×
355
                                                                scope.setTag('auth_method', 'check session');
×
356
                                                                Sentry.captureMessage(getErrorString(err));
×
357
                                                        });
×
358
                                                        this[clearNotedLoginState]();
×
359
                                                        this[handleUnknownError](err);
×
360
                                                        resolve();
×
361
                                                }
×
362
                                        } else {
×
363
                                                // Successful authentication
×
364
                                                this[setAuthData](result);
×
365
                                                this[noteLoggedIn]();
×
366
                                                resolve();
×
367
                                        }
×
368
                                });
×
369
                        });
×
370
                });
×
371

372
                // Once the promise completes, stop tracking it
×
373
                this[sessionPromise].finally(() => {
×
374
                        this[sessionPromise] = null;
×
375
                });
×
376

377
                return this[sessionPromise];
×
378
        }
×
379

380
        // Receive a callback to be called in case of an unknown error
1✔
381
        onError(callback) {
1✔
382
                this[errorCallbacks].push(callback);
3✔
383
        }
3✔
384

385
        // Call error callbacks with error information
1✔
386
        [handleUnknownError](error) {
1✔
387
                logFormatter(error, 'error');
×
388
                this[errorCallbacks].map(callback => callback({
×
389
                        error,
×
390
                        errorString: getErrorString(error),
×
391
                        eventId: Sentry.lastEventId(),
×
392
                        user: this.user,
×
393
                }));
×
394
        }
×
395
}
1✔
396

397
// Provide a mock class for testing and disabling auth0 usage
1✔
398
export const MockKvAuth0 = {
1✔
399
        enabled: false,
1✔
400
        user: {},
1✔
401
        accessToken: '',
1✔
402
        getKivaId: () => undefined,
1✔
403
        getLastLogin: () => 0,
1✔
404
        getMfaEnrollToken: () => Promise.resolve({}),
1✔
405
        fakeAuthAllowed: () => false,
1✔
406
        getFakeAuthCookieValue: () => undefined,
1✔
407
        getFakeIdTokenPayload: () => undefined,
1✔
408
        getSyncCookieValue: () => null,
1✔
409
        isNotedLoggedIn: () => false,
1✔
410
        isNotedLoggedOut: () => false,
1✔
411
        isNotedUserSessionUser: () => true,
1✔
412
        checkSession: () => Promise.resolve({}),
1✔
413
        popupLogin: () => Promise.resolve({}),
1✔
414
        popupCallback: () => Promise.resolve({}),
1✔
415
        onError: () => { },
1✔
416
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc