Skip to content

Commit 73d380a

Browse files
author
Llorenç Muntaner
authored
Auth with Generic Open ID (#3297)
1 parent 21d4aa0 commit 73d380a

File tree

3 files changed

+150
-8
lines changed

3 files changed

+150
-8
lines changed

src/frontend/src/lib/components/wizards/auth/AuthWizard.svelte

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import { MigrationWizard } from "$lib/components/wizards/migration";
1515
import { isWebAuthnCancelError } from "$lib/utils/webAuthnErrorUtils";
1616
import { isOpenIdCancelError } from "$lib/utils/openID";
17+
import type { OpenIdConfig } from "$lib/generated/internet_identity_types";
1718
1819
interface Props {
1920
isAuthenticating?: boolean;
@@ -105,6 +106,25 @@
105106
isAuthenticating = false;
106107
}
107108
};
109+
110+
const handleContinueWithOpenId = async (
111+
config: OpenIdConfig,
112+
): Promise<void | "cancelled"> => {
113+
isAuthenticating = true;
114+
try {
115+
const { identityNumber, type } =
116+
await authFlow.continueWithOpenId(config);
117+
(type === "signUp" ? onSignUp : onSignIn)(identityNumber);
118+
} catch (error) {
119+
if (isOpenIdCancelError(error)) {
120+
return "cancelled";
121+
}
122+
onError(error); // Propagate unhandled errors to parent component
123+
} finally {
124+
isAuthenticating = false;
125+
}
126+
};
127+
108128
const handleRegistered = async (identityNumber: bigint) => {
109129
if (canisterConfig.feature_flag_continue_from_another_device[0] === true) {
110130
onSignIn(identityNumber);
@@ -152,6 +172,7 @@
152172
<PickAuthenticationMethod
153173
setupOrUseExistingPasskey={authFlow.setupOrUseExistingPasskey}
154174
continueWithGoogle={handleContinueWithGoogle}
175+
continueWithOpenId={handleContinueWithOpenId}
155176
migrate={() => (isMigrating = true)}
156177
/>
157178
{/if}

src/frontend/src/lib/components/wizards/auth/views/PickAuthenticationMethod.svelte

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,31 @@
66
import Alert from "$lib/components/ui/Alert.svelte";
77
import ProgressRing from "$lib/components/ui/ProgressRing.svelte";
88
import { canisterConfig } from "$lib/globals";
9-
import { ENABLE_MIGRATE_FLOW } from "$lib/state/featureFlags";
9+
import {
10+
ENABLE_GENERIC_OPEN_ID,
11+
ENABLE_MIGRATE_FLOW,
12+
} from "$lib/state/featureFlags";
1013
import { waitFor } from "$lib/utils/utils";
1114
import Tooltip from "$lib/components/ui/Tooltip.svelte";
15+
import type { OpenIdConfig } from "$lib/generated/internet_identity_types";
1216
1317
interface Props {
1418
setupOrUseExistingPasskey: () => void;
1519
continueWithGoogle: () => Promise<void | "cancelled">;
20+
continueWithOpenId: (config: OpenIdConfig) => Promise<void | "cancelled">;
1621
migrate: () => void;
1722
}
1823
19-
const { setupOrUseExistingPasskey, continueWithGoogle, migrate }: Props =
20-
$props();
24+
const {
25+
setupOrUseExistingPasskey,
26+
continueWithGoogle,
27+
continueWithOpenId,
28+
migrate,
29+
}: Props = $props();
2130
2231
let isAuthenticating = $state(false);
2332
let isCancelled = $state(false);
33+
let cancelledProviderId = $state<string | null>(null);
2434
2535
const handleContinueWithGoogle = async () => {
2636
isAuthenticating = true;
@@ -34,8 +44,22 @@
3444
}
3545
};
3646
47+
const handleContinueWithOpenId = async (config: OpenIdConfig) => {
48+
isAuthenticating = true;
49+
const result = await continueWithOpenId(config);
50+
isAuthenticating = false;
51+
52+
if (result === "cancelled") {
53+
cancelledProviderId = config.client_id;
54+
await waitFor(4000);
55+
cancelledProviderId = null;
56+
}
57+
};
58+
3759
const supportsPasskeys = nonNullish(window.PublicKeyCredential);
38-
const showGoogleButton = canisterConfig.openid_google?.[0]?.[0];
60+
const showGoogleButton =
61+
canisterConfig.openid_google?.[0]?.[0] && !ENABLE_GENERIC_OPEN_ID;
62+
const openIdProviders = canisterConfig.openid_configs?.[0] || [];
3963
</script>
4064

4165
<div class="flex flex-col items-stretch gap-6">
@@ -47,10 +71,36 @@
4771
/>
4872
{/if}
4973
<div class="flex flex-col items-stretch gap-3">
74+
{#if $ENABLE_GENERIC_OPEN_ID}
75+
<div class="flex flex-row flex-nowrap justify-stretch gap-3">
76+
{#each openIdProviders as provider}
77+
<Tooltip
78+
label="Interaction canceled. Please try again."
79+
hidden={cancelledProviderId !== provider.client_id}
80+
manual
81+
>
82+
<Button
83+
onclick={() => handleContinueWithOpenId(provider)}
84+
variant="secondary"
85+
disabled={isAuthenticating}
86+
size="xl"
87+
class="flex-1"
88+
>
89+
{#if isAuthenticating}
90+
<ProgressRing />
91+
{:else if provider.logo}
92+
{@html provider.logo}
93+
{/if}
94+
</Button>
95+
</Tooltip>
96+
{/each}
97+
</div>
98+
{/if}
5099
<Button
51100
onclick={setupOrUseExistingPasskey}
52101
disabled={!supportsPasskeys || isAuthenticating}
53102
size="xl"
103+
variant={$ENABLE_GENERIC_OPEN_ID ? "secondary" : "primary"}
54104
>
55105
<PasskeyIcon />
56106
Continue with Passkey

src/frontend/src/lib/flows/authFlow.svelte.ts

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ import {
2727
IdRegFinishError,
2828
IdRegStartError,
2929
OpenIdDelegationError,
30+
OpenIdConfig,
3031
} from "$lib/generated/internet_identity_types";
31-
import { createGoogleRequestConfig, requestJWT } from "$lib/utils/openID";
32+
import {
33+
createGoogleRequestConfig,
34+
requestJWT,
35+
RequestConfig,
36+
} from "$lib/utils/openID";
3237

3338
export class AuthFlow {
3439
#view = $state<
@@ -201,7 +206,73 @@ export class AuthFlow {
201206
AuthenticationV2Events.RegisterWithGoogle,
202207
);
203208
await this.#startRegistration();
204-
const identityNumber = await this.#registerWithGoogle(jwt);
209+
const identityNumber = await this.#registerWithOpenId(jwt);
210+
return { identityNumber, type: "signUp" };
211+
}
212+
this.#view = "chooseMethod";
213+
throw error;
214+
}
215+
};
216+
217+
continueWithOpenId = async (
218+
config: OpenIdConfig,
219+
): Promise<{
220+
identityNumber: bigint;
221+
type: "signIn" | "signUp";
222+
}> => {
223+
let jwt: string | undefined = undefined;
224+
// Convert OpenIdConfig to RequestConfig
225+
const requestConfig: RequestConfig = {
226+
clientId: config.client_id,
227+
authURL: config.auth_uri,
228+
configURL: config.fedcm_uri?.[0],
229+
};
230+
// Create two try-catch blocks to avoid double-triggering the analytics.
231+
try {
232+
this.#systemOverlay = true;
233+
jwt = await requestJWT(requestConfig, {
234+
nonce: get(sessionStore).nonce,
235+
mediation: "required",
236+
});
237+
} catch (error) {
238+
this.#view = "chooseMethod";
239+
throw error;
240+
} finally {
241+
this.#systemOverlay = false;
242+
// Moved after `requestJWT` to avoid Safari from blocking the popup.
243+
authenticationV2Funnel.trigger(AuthenticationV2Events.ContinueWithGoogle);
244+
}
245+
try {
246+
const { identity, identityNumber, iss, sub } = await authenticateWithJWT({
247+
canisterId,
248+
session: get(sessionStore),
249+
jwt,
250+
});
251+
// If the previous call succeeds, it means the OpenID user already exists in II.
252+
// Therefore, they are logging in.
253+
// If the call fails, it means the OpenID user does not exist in II.
254+
// In that case, we register them.
255+
authenticationV2Funnel.trigger(AuthenticationV2Events.LoginWithGoogle);
256+
authenticationStore.set({ identity, identityNumber });
257+
const info =
258+
await get(authenticatedStore).actor.get_anchor_info(identityNumber);
259+
lastUsedIdentitiesStore.addLastUsedIdentity({
260+
identityNumber,
261+
name: info.name[0],
262+
authMethod: { openid: { iss, sub } },
263+
});
264+
return { identityNumber, type: "signIn" };
265+
} catch (error) {
266+
if (
267+
isCanisterError<OpenIdDelegationError>(error) &&
268+
error.type === "NoSuchAnchor" &&
269+
nonNullish(jwt)
270+
) {
271+
authenticationV2Funnel.trigger(
272+
AuthenticationV2Events.RegisterWithGoogle,
273+
);
274+
await this.#startRegistration();
275+
const identityNumber = await this.#registerWithOpenId(jwt);
205276
return { identityNumber, type: "signUp" };
206277
}
207278
this.#view = "chooseMethod";
@@ -335,7 +406,7 @@ export class AuthFlow {
335406
}
336407
};
337408

338-
#registerWithGoogle = async (jwt: string): Promise<bigint> => {
409+
#registerWithOpenId = async (jwt: string): Promise<bigint> => {
339410
try {
340411
await get(sessionStore)
341412
.actor.openid_identity_registration_finish({
@@ -372,7 +443,7 @@ export class AuthFlow {
372443
await this.#solveCaptcha(
373444
`data:image/png;base64,${nextStep.CheckCaptcha.captcha_png_base64}`,
374445
);
375-
return this.#registerWithGoogle(jwt);
446+
return this.#registerWithOpenId(jwt);
376447
}
377448
}
378449
throw error;

0 commit comments

Comments
 (0)