Skip to content

Commit 552d9c3

Browse files
committed
chore(google-one-tap): Use monadic errors when parsing token
1 parent 61e0112 commit 552d9c3

File tree

2 files changed

+33
-28
lines changed

2 files changed

+33
-28
lines changed

dotcom-rendering/src/components/GoogleOneTap.importable.tsx

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { CountryCode } from '@guardian/libs';
2-
import { log } from '@guardian/libs';
2+
import { isObject, log } from '@guardian/libs';
33
import type { TAction, TComponentType } from '@guardian/ophan-tracker-js';
44
import { submitComponentEvent } from '../client/ophan/ophan';
5+
import type { Result } from '../lib/result';
6+
import { error, ok, okOrThrow } from '../lib/result';
57
import { useIsSignedIn } from '../lib/useAuthStatus';
68
import { useConsent } from '../lib/useConsent';
79
import { useCountryCode } from '../lib/useCountryCode';
@@ -59,13 +61,21 @@ const getStage = (hostname: string): StageType => {
5961
* @param token A JWT Token
6062
* @returns extracted email address
6163
*/
62-
export const extractEmailFromToken = (token: string): string | undefined => {
64+
export const extractEmailFromToken = (
65+
token: string,
66+
): Result<'ParsingError', string> => {
6367
const payload = token.split('.')[1];
64-
if (!payload) return;
65-
const decoded = atob(payload);
66-
const parsed = JSON.parse(decoded) as Record<string, unknown>;
67-
if (typeof parsed.email !== 'string') return;
68-
return parsed.email;
68+
if (!payload) return error('ParsingError');
69+
try {
70+
const decoded = Buffer.from(payload, 'base64').toString();
71+
const parsed = JSON.parse(decoded) as unknown;
72+
if (!isObject(parsed) || typeof parsed.email !== 'string') {
73+
return error('ParsingError');
74+
}
75+
return ok(parsed.email);
76+
} catch (e) {
77+
return error('ParsingError');
78+
}
6979
};
7080

7181
export const getRedirectUrl = ({
@@ -189,7 +199,7 @@ export const initializeFedCM = async ({
189199
providers: getProviders(stage),
190200
},
191201
})
192-
.catch((error) => {
202+
.catch((e) => {
193203
/**
194204
* The fedcm API hides issues with the user's federated login state
195205
* behind a generic NetworkError. This error is thrown up to 60
@@ -201,13 +211,13 @@ export const initializeFedCM = async ({
201211
* Unfortunately for us it means we can't differentiate between
202212
* a genuine network error and a user declining the FedCM prompt.
203213
*/
204-
if (error instanceof Error && error.name === 'NetworkError') {
214+
if (e instanceof Error && e.name === 'NetworkError') {
205215
log(
206216
'identity',
207217
'FedCM prompt failed, potentially due to user declining',
208218
);
209219
} else {
210-
throw error;
220+
throw e;
211221
}
212222
});
213223

@@ -216,16 +226,10 @@ export const initializeFedCM = async ({
216226
credentials,
217227
});
218228

219-
const signInEmail = extractEmailFromToken(credentials.token);
220-
if (signInEmail) {
221-
log('identity', `FedCM ID token for ${signInEmail} received`);
222-
} else {
223-
log(
224-
'identity',
225-
'FedCM token received but email could not be extracted from token',
226-
);
227-
return;
228-
}
229+
const signInEmail = okOrThrow(
230+
extractEmailFromToken(credentials.token),
231+
'Failed to extract email from FedCM token',
232+
);
229233

230234
await submitComponentEvent(
231235
{

dotcom-rendering/src/components/GoogleOneTap.test.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { submitComponentEvent as submitComponentEventMock } from '../client/ophan/ophan';
2+
import { error, ok } from '../lib/result';
23
import {
34
extractEmailFromToken,
45
getRedirectUrl,
@@ -79,11 +80,11 @@ describe('GoogleOneTap', () => {
7980
extractEmailFromToken(
8081
'NULL.eyJlbWFpbCI6InZhbGlkQGVtYWlsLmNvbSJ9.NULL',
8182
),
82-
).toEqual('[email protected]');
83+
).toEqual(ok('[email protected]'));
8384
});
8485

8586
it('should return undefined from a malformed JWT token', () => {
86-
expect(extractEmailFromToken('NULL')).toEqual(undefined);
87+
expect(extractEmailFromToken('NULL')).toEqual(error('ParsingError'));
8788
});
8889

8990
it('should initializeFedCM and redirect to Gateway with token on success', async () => {
@@ -144,10 +145,10 @@ describe('GoogleOneTap', () => {
144145
});
145146

146147
it('should initializeFedCM and not redirect to Gateway with token on failure', async () => {
147-
const error = new Error('Network Error');
148-
error.name = 'NetworkError';
148+
const e = new Error('Network Error');
149+
e.name = 'NetworkError';
149150

150-
const navigatorGet = jest.fn(() => Promise.reject(error));
151+
const navigatorGet = jest.fn(() => Promise.reject(e));
151152
const locationReplace = jest.fn();
152153

153154
mockWindow({
@@ -199,10 +200,10 @@ describe('GoogleOneTap', () => {
199200
});
200201

201202
it('should initializeFedCM and throw error when unexpected', async () => {
202-
const error = new Error('window.navigator.credentials.get failed');
203-
error.name = 'DOMException';
203+
const e = new Error('window.navigator.credentials.get failed');
204+
e.name = 'DOMException';
204205

205-
const navigatorGet = jest.fn(() => Promise.reject(error));
206+
const navigatorGet = jest.fn(() => Promise.reject(e));
206207
const locationReplace = jest.fn();
207208

208209
mockWindow({

0 commit comments

Comments
 (0)