Skip to content

Commit 709eda7

Browse files
committed
feat: Use built-in FedCM API instead of Google GSI
1 parent 5c26974 commit 709eda7

File tree

2 files changed

+76
-102
lines changed

2 files changed

+76
-102
lines changed

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

Lines changed: 75 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,51 @@
1-
import { loadScript, log } from '@guardian/libs';
1+
import { log } from '@guardian/libs';
22
import { useEffect } from 'react';
33
import { useIsSignedIn } from '../lib/useAuthStatus';
4-
5-
const getGSIConfiguration = (): { clientId: string; loginUri: string } => {
6-
switch (window.guardian.config.stage) {
7-
case 'PROD':
8-
return {
9-
clientId: 'PROD CLIENT ID',
10-
loginUri: 'https://profile.thegulocal.com/signin/google',
11-
};
12-
case 'CODE':
13-
return {
14-
clientId: 'CODE CLIENT ID',
15-
loginUri: 'https://profile.thegulocal.com/signin/google',
16-
};
4+
import type { StageType } from '../types/config';
5+
6+
const getFedCMProviders = (stage: StageType): IdentityProviderConfig[] => {
7+
switch (stage) {
8+
// case 'PROD':
9+
// return [
10+
// {
11+
// configURL: 'https://accounts.google.com/gsi/fedcm.json',
12+
// clientId: '774465807556.apps.googleusercontent.com',
13+
// },
14+
// ];
15+
// case 'CODE':
16+
// return [
17+
// {
18+
// configURL: 'https://accounts.google.com/gsi/fedcm.json',
19+
// clientId: '774465807556-pkevncqpfs9486ms0bo5q1f2g9vhpior.apps.googleusercontent.com',
20+
// },
21+
// ];
1722
default:
18-
return {
19-
clientId:
20-
'774465807556-pkevncqpfs9486ms0bo5q1f2g9vhpior.apps.googleusercontent.com',
21-
loginUri: 'https://profile.thegulocal.com/signin/google',
22-
};
23+
return [
24+
{
25+
configURL: 'https://accounts.google.com/gsi/fedcm.json',
26+
clientId:
27+
'774465807556-pkevncqpfs9486ms0bo5q1f2g9vhpior.apps.googleusercontent.com',
28+
},
29+
];
2330
}
2431
};
2532

26-
const initializeGoogleOneTap = () => (response: { credential: string }) => {
27-
const { credential } = response;
28-
29-
const queryParams = new URLSearchParams();
30-
queryParams.append('got', credential);
31-
queryParams.append('returnUrl', window.location.href);
32-
33-
window.location.replace(
34-
`https://profile.thegulocal.com/signin/google?${queryParams.toString()}`,
35-
);
33+
type IdentityCredentials = {
34+
token: string;
3635
};
3736

38-
type PromptMomentNotification = {
39-
isSkippedMoment: () => boolean;
40-
isDismissedMoment: () => boolean;
41-
getDismissedReason: () =>
42-
| 'credential_returned'
43-
| 'cancel_called'
44-
| 'flow_restarted';
45-
getMomentType: () => 'display' | 'skipped' | 'dismissed';
37+
type IdentityProviderConfig = {
38+
configURL: string;
39+
clientId: string;
4640
};
4741

48-
export type GoogleIdentityService = {
49-
initialize: (config: {
50-
client_id: string;
51-
login_uri: string;
52-
callback: (response: { credential: string }) => void;
53-
auto_select?: boolean;
54-
cancel_on_tap_outside?: boolean;
55-
use_fedcm_for_prompt?: boolean;
56-
}) => void;
57-
prompt: (
58-
callback: (momentNotification: PromptMomentNotification) => void,
59-
) => void;
60-
};
61-
62-
const loadGSI = async (): Promise<GoogleIdentityService> => {
63-
log('identity', 'Loading Google Sign-in Services (GSI)');
64-
// TODO: Can we invoke the built-in FedCM API instead of using GSI?
65-
// This would reduce our dpenedency on a third-party library and all of the privacy
66-
// implications that come with it. It would also save loading ~80KB of JS.
67-
await loadScript('https://accounts.google.com/gsi/client').catch((e) => {
68-
throw new Error(
69-
`Failed to initialize Google One Tap: failed to load GSI`,
70-
{ cause: e },
71-
);
72-
});
73-
74-
if (!window.google?.accounts?.id) {
75-
throw new Error('Failed to initialize Google One Tap: GSI not found');
76-
}
77-
78-
log('identity', 'Loaded Google Sign-in Services (GSI)');
79-
return window.google.accounts.id;
42+
type CredentialsProvider = {
43+
get: (options: {
44+
identity: {
45+
context: 'signin';
46+
providers: IdentityProviderConfig[];
47+
};
48+
}) => Promise<IdentityCredentials>;
8049
};
8150

8251
export const GoogleOneTap = () => {
@@ -98,34 +67,44 @@ export const GoogleOneTap = () => {
9867
return;
9968
}
10069

101-
const { clientId, loginUri } = getGSIConfiguration();
102-
103-
void loadGSI().then((gsi) => {
104-
log('identity', 'Initializing Google One Tap', {
105-
clientId,
106-
loginUri,
107-
});
108-
109-
gsi.initialize({
110-
client_id: clientId,
111-
login_uri: loginUri,
112-
callback: initializeGoogleOneTap,
113-
auto_select: true,
114-
cancel_on_tap_outside: false,
115-
use_fedcm_for_prompt: true,
116-
});
117-
118-
log('identity', 'Requesting Google One Tap prompt');
119-
gsi.prompt((notifcation) => {
120-
// TODO: Handle tracking of the prompt moment notification. Ophan?
121-
log('identity', 'Google One Tap prompt notification received', {
122-
isSkippedMoment: notifcation.isSkippedMoment(),
123-
isDismissedMoment: notifcation.isDismissedMoment(),
124-
dismissedReason: notifcation.getDismissedReason(),
125-
momentType: notifcation.getMomentType(),
126-
});
70+
const credentialsProvider = window.navigator
71+
.credentials as unknown as CredentialsProvider;
72+
73+
void credentialsProvider
74+
.get({
75+
identity: {
76+
context: 'signin',
77+
providers: getFedCMProviders(window.guardian.config.stage),
78+
},
79+
})
80+
.catch((error) => {
81+
/**
82+
* The fedcm API hides issues with the user's federated login state
83+
* behind a generic NetworkError. This error is thrown up to 60
84+
* seconds after the prompt is triggered to avoid timing attacks.
85+
*
86+
* This allows the browser to avoid leaking sensitive information
87+
* about the user's login state to the website.
88+
*
89+
* Unfortunately for us it means we can't differentiate between
90+
* a genuine network error and a user declining the FedCM prompt.
91+
*/
92+
if (error instanceof Error && error.name === 'NetworkError') {
93+
log(
94+
'identity',
95+
'FedCM prompt failed, potentially due to user declining',
96+
);
97+
}
98+
})
99+
.then((credentials) => {
100+
if (credentials) {
101+
log('identity', 'FedCM credentials received', {
102+
credentials,
103+
});
104+
} else {
105+
log('identity', 'No FedCM credentials received');
106+
}
127107
});
128-
});
129108
}, [isSignedIn]);
130109

131110
return <></>;

dotcom-rendering/window.guardian.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import type {
77
} from '@guardian/libs';
88
import type ophan from '@guardian/ophan-tracker-js';
99
import type { WeeklyArticleHistory } from '@guardian/support-dotcom-components/dist/dotcom/types';
10-
import type { GoogleIdentityService } from './src/components/GoogleOneTap.importable';
1110
import type { google } from './src/components/YoutubeAtom/ima';
1211
import type { DailyArticleHistory } from './src/lib/dailyArticleCount';
1312
import type { ReaderRevenueDevUtils } from './src/lib/readerRevenueDevUtils';
@@ -68,11 +67,7 @@ declare global {
6867
) => boolean;
6968
};
7069
mockLiveUpdate: (data: LiveUpdateType) => void;
71-
google?: typeof google & {
72-
accounts?: {
73-
id?: GoogleIdentityService;
74-
};
75-
};
70+
google?: typeof google;
7671
YT?: typeof YT;
7772
onYouTubeIframeAPIReady?: () => void;
7873
}

0 commit comments

Comments
 (0)