-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable PIN registration with query param #2711
Changes from all commits
cc5ddfd
1a45f61
8c93cba
9821796
6a7a77a
1d072b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,18 @@ | ||
import { AuthnMethodData } from "$generated/internet_identity_types"; | ||
import { withLoader } from "$src/components/loader"; | ||
import { PinIdentityMaterial } from "$src/crypto/pinIdentity"; | ||
import { ENABLE_PIN_QUERY_PARAM_KEY } from "$src/config"; | ||
import { | ||
PinIdentityMaterial, | ||
constructPinIdentity, | ||
} from "$src/crypto/pinIdentity"; | ||
import { idbStorePinIdentityMaterial } from "$src/flows/pin/idb"; | ||
import { registerDisabled } from "$src/flows/registerDisabled"; | ||
import { I18n } from "$src/i18n"; | ||
import { setAnchorUsed } from "$src/storage"; | ||
import { passkeyAuthnMethodData } from "$src/utils/authnMethodData"; | ||
import { | ||
passkeyAuthnMethodData, | ||
pinAuthnMethodData, | ||
} from "$src/utils/authnMethodData"; | ||
import { | ||
AlreadyInProgress, | ||
ApiError, | ||
|
@@ -23,7 +31,10 @@ import { | |
import { SignIdentity } from "@dfinity/agent"; | ||
import { ECDSAKeyIdentity } from "@dfinity/identity"; | ||
import { nonNullish } from "@dfinity/utils"; | ||
import { TemplateResult } from "lit-html"; | ||
import type { UAParser } from "ua-parser-js"; | ||
import { tempKeyWarningBox } from "../manage/tempKeys"; | ||
import { setPinFlow } from "../pin/setPin"; | ||
import { precomputeFirst, promptCaptcha } from "./captcha"; | ||
import { displayUserNumberWarmup } from "./finish"; | ||
import { savePasskeyOrPin } from "./passkey"; | ||
|
@@ -33,9 +44,9 @@ export const registerFlow = async ({ | |
identityRegistrationStart, | ||
checkCaptcha, | ||
identityRegistrationFinish, | ||
storePinIdentity: _storePinIdentity, | ||
storePinIdentity, | ||
registrationAllowed, | ||
pinAllowed: _pinAllowed, | ||
pinAllowed, | ||
uaParser, | ||
}: { | ||
identityRegistrationStart: () => Promise< | ||
|
@@ -98,24 +109,82 @@ export const registerFlow = async ({ | |
const flowStart = precomputeFirst(() => identityRegistrationStart()); | ||
|
||
const displayUserNumber = displayUserNumberWarmup(); | ||
const identity = await savePasskeyOrPin(); | ||
if (identity === undefined) { | ||
// TODO: Return something meaningful if getting the identity fails | ||
const savePasskeyResult = await savePasskeyOrPin({ | ||
pinAllowed: await pinAllowed(), | ||
}); | ||
if (savePasskeyResult === "canceled") { | ||
return "canceled"; | ||
} | ||
const alias = await inferPasskeyAlias({ | ||
authenticatorType: identity.getAuthenticatorAttachment(), | ||
userAgent: navigator.userAgent, | ||
uaParser, | ||
}); | ||
const result_ = await (async () => { | ||
if (savePasskeyResult === "pin") { | ||
const pinResult = await setPinFlow(); | ||
if (pinResult.tag === "canceled") { | ||
return "canceled"; | ||
} | ||
|
||
const authnMethodData = passkeyAuthnMethodData({ | ||
alias, | ||
pubKey: identity.getPublicKey().toDer(), | ||
credentialId: identity.rawId, | ||
authenticatorAttachment: identity.getAuthenticatorAttachment(), | ||
}); | ||
const authnMethod = "passkey" as const; | ||
pinResult.tag satisfies "ok"; | ||
|
||
// XXX: this withLoader could be replaced with one that indicates what's happening (like the | ||
// "Hang tight, ..." spinner) | ||
const { identity, pinIdentityMaterial } = await withLoader(() => | ||
constructPinIdentity(pinResult) | ||
); | ||
const alias = await inferPinAlias({ | ||
userAgent: navigator.userAgent, | ||
uaParser, | ||
}); | ||
return { | ||
identity, | ||
authnMethodData: pinAuthnMethodData({ | ||
alias, | ||
pubKey: identity.getPublicKey().toDer(), | ||
}), | ||
finalizeIdentity: (userNumber: bigint) => | ||
storePinIdentity({ userNumber, pinIdentityMaterial }), | ||
finishSlot: tempKeyWarningBox({ i18n: new I18n() }), | ||
authnMethod: "pin" as const, | ||
}; | ||
} else { | ||
const identity = savePasskeyResult; | ||
// TODO: Return something meaningful if getting the passkey identity fails | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only new line that was not just reverted. Because the view can return This is in the current code as well. The way this error was handled before was to never resolve the promise and show a toast error. |
||
if (identity === undefined) { | ||
return "canceled"; | ||
} | ||
const alias = await inferPasskeyAlias({ | ||
authenticatorType: identity.getAuthenticatorAttachment(), | ||
userAgent: navigator.userAgent, | ||
uaParser, | ||
}); | ||
return { | ||
identity, | ||
authnMethodData: passkeyAuthnMethodData({ | ||
alias, | ||
pubKey: identity.getPublicKey().toDer(), | ||
credentialId: identity.rawId, | ||
authenticatorAttachment: identity.getAuthenticatorAttachment(), | ||
}), | ||
authnMethod: "passkey" as const, | ||
}; | ||
} | ||
})(); | ||
|
||
if (result_ === "canceled") { | ||
return "canceled"; | ||
} | ||
|
||
const { | ||
identity, | ||
authnMethodData, | ||
finalizeIdentity, | ||
finishSlot, | ||
authnMethod, | ||
}: { | ||
identity: SignIdentity; | ||
authnMethodData: AuthnMethodData; | ||
finalizeIdentity?: (userNumber: bigint) => Promise<void>; | ||
finishSlot?: TemplateResult; | ||
authnMethod: "pin" | "passkey"; | ||
} = result_; | ||
|
||
const startResult = await flowStart(); | ||
if (startResult.kind !== "registrationFlowStepSuccess") { | ||
|
@@ -150,6 +219,7 @@ export const registerFlow = async ({ | |
result.kind satisfies "loginSuccess"; | ||
|
||
const userNumber = result.userNumber; | ||
await finalizeIdentity?.(userNumber); | ||
// We don't want to nudge the user with the recovery phrase warning page | ||
// right after they've created their anchor. | ||
result.connection.updateIdentityMetadata({ | ||
|
@@ -162,6 +232,7 @@ export const registerFlow = async ({ | |
); | ||
await displayUserNumber({ | ||
userNumber, | ||
marketingIntroSlot: finishSlot, | ||
}); | ||
return { ...result, authnMethod }; | ||
}; | ||
|
@@ -180,6 +251,10 @@ export const getRegisterFlowOpts = async ({ | |
const tempIdentity = await ECDSAKeyIdentity.generate({ | ||
extractable: false, | ||
}); | ||
const params = new URLSearchParams(window.location.search); | ||
// Only allow PIN if query param is set and the request allows it | ||
const allowPinRegistration = | ||
params.get(ENABLE_PIN_QUERY_PARAM_KEY) !== null && allowPinAuthentication; | ||
return { | ||
/** Check that the current origin is not the explicit canister id or a raw url. | ||
* Explanation why we need to do this: | ||
|
@@ -192,7 +267,7 @@ export const getRegisterFlowOpts = async ({ | |
pinAllowed: () => | ||
// If pin auth is disallowed by the authenticating dapp then abort, otherwise check | ||
// if pin auth is allowed for the user agent | ||
allowPinAuthentication | ||
allowPinRegistration | ||
? pinRegisterAllowed({ userAgent: navigator.userAgent, uaParser }) | ||
: Promise.resolve(false), | ||
identityRegistrationStart: async () => | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,11 +97,32 @@ const savePasskeyTemplate = ({ | |
export const savePasskeyPage = renderPage(savePasskeyTemplate); | ||
|
||
// Prompt the user to create a WebAuthn identity or a PIN identity (if allowed) | ||
export const savePasskeyOrPin = async (): Promise< | ||
IIWebAuthnIdentity | undefined | ||
> => { | ||
export const savePasskeyOrPin = async ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is reverted to the old logic if |
||
pinAllowed, | ||
}: { | ||
pinAllowed: boolean; | ||
}): Promise<IIWebAuthnIdentity | "pin" | "canceled" | undefined> => { | ||
if (pinAllowed) { | ||
return new Promise((resolve) => { | ||
return savePasskeyPage({ | ||
i18n: new I18n(), | ||
cancel: () => resolve("canceled"), | ||
scrollToTop: true, | ||
constructPasskey: async () => { | ||
try { | ||
const identity = await withLoader(() => constructIdentity({})); | ||
resolve(identity); | ||
} catch (e) { | ||
toast.error(errorMessage(e)); | ||
} | ||
}, | ||
constructPin: pinAllowed ? () => resolve("pin") : undefined, | ||
}); | ||
}); | ||
} | ||
try { | ||
return await withLoader(() => constructIdentity({})); | ||
const identity = await withLoader(() => constructIdentity({})); | ||
return identity; | ||
} catch (e) { | ||
toast.error(errorMessage(e)); | ||
return undefined; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,39 +12,33 @@ import { | |
switchToPopup, | ||
wipeStorage, | ||
} from "./util"; | ||
import { | ||
AuthenticateView, | ||
DemoAppView, | ||
MainView, | ||
PinAuthView, | ||
RegisterView, | ||
WelcomeView, | ||
} from "./views"; | ||
import { AuthenticateView, DemoAppView, MainView, PinAuthView } from "./views"; | ||
|
||
const DEFAULT_PIN_DEVICE_NAME = "Chrome on Mac OS"; | ||
// Same as in frontend/src/config.ts | ||
const ENABLE_PIN_QUERY_PARAM_KEY = "enablePin"; | ||
|
||
// TODO: GIX-3138 Clean up after release | ||
// TODO: Test login with PIN only GIX-3139 | ||
test.skip("PIN registration not enabled on non-Apple device", async () => { | ||
test("PIN registration not enabled on non-Apple device", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
await browser.url(II_URL); | ||
const welcomeView = new WelcomeView(browser); | ||
await welcomeView.waitForDisplay(); | ||
await welcomeView.register(); | ||
const registerView = new RegisterView(browser); | ||
await registerView.waitForDisplay(); | ||
await registerView.assertPinRegistrationNotShown(); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
// The PIN registration flow should not be enabled and go directly to login with passkey | ||
await addVirtualAuthenticator(browser); | ||
await FLOWS.registerNewIdentityWelcomeView(browser); | ||
const mainView = new MainView(browser); | ||
await mainView.waitForDeviceDisplay(DEVICE_NAME1); | ||
await mainView.logout(); | ||
}, EDGE_USER_AGENT); | ||
}, 300_000); | ||
|
||
// The PIN auth feature is only enabled for Apple specific user agents, so tests set the user | ||
// agent to chrome on macOS | ||
|
||
test.skip("Register and Log in with PIN identity", async () => { | ||
test("Register and Log in with PIN identity", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
|
||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
const userNumber = await FLOWS.registerPinWelcomeView(browser, pin); | ||
const mainView = new MainView(browser); | ||
await mainView.waitForDisplay(); // we should be logged in | ||
|
@@ -55,10 +49,10 @@ test.skip("Register and Log in with PIN identity", async () => { | |
}, APPLE_USER_AGENT); | ||
}, 300_000); | ||
|
||
test.skip("Register with PIN and login without prefilled identity number", async () => { | ||
test("Register with PIN and login without prefilled identity number", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
const userNumber = await FLOWS.registerPinWelcomeView(browser, pin); | ||
|
||
const mainView = new MainView(browser); | ||
|
@@ -68,18 +62,18 @@ test.skip("Register with PIN and login without prefilled identity number", async | |
await wipeStorage(browser); | ||
|
||
// load the II page again | ||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
await FLOWS.loginPinWelcomeView(userNumber, pin, browser); | ||
await mainView.waitForTempKeyDisplay(DEFAULT_PIN_DEVICE_NAME); | ||
}, APPLE_USER_AGENT); | ||
}, 300_000); | ||
|
||
test.skip("Register and log in with PIN identity, retry on wrong PIN", async () => { | ||
test("Register and log in with PIN identity, retry on wrong PIN", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
const wrongPin = "456321"; | ||
|
||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
const userNumber = await FLOWS.registerPinWelcomeView(browser, pin); | ||
const mainView = new MainView(browser); | ||
await mainView.waitForDisplay(); // we should be logged in | ||
|
@@ -100,12 +94,12 @@ test.skip("Register and log in with PIN identity, retry on wrong PIN", async () | |
}, APPLE_USER_AGENT); | ||
}, 300_000); | ||
|
||
test.skip("Should not prompt for PIN after deleting temp key", async () => { | ||
test("Should not prompt for PIN after deleting temp key", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
await addVirtualAuthenticator(browser); | ||
|
||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
const userNumber = await FLOWS.registerPinWelcomeView(browser, pin); | ||
const mainView = new MainView(browser); | ||
await mainView.waitForDisplay(); // we should be logged in | ||
|
@@ -123,32 +117,11 @@ test.skip("Should not prompt for PIN after deleting temp key", async () => { | |
}, APPLE_USER_AGENT); | ||
}, 300_000); | ||
|
||
test.skip("Log into client application using PIN registration flow", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't be enabled in the foreseeable future. |
||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
|
||
const demoAppView = new DemoAppView(browser); | ||
await demoAppView.open(TEST_APP_NICE_URL, II_URL); | ||
await demoAppView.waitForDisplay(); | ||
expect(await demoAppView.getPrincipal()).toBe(""); | ||
await demoAppView.signin(); | ||
await switchToPopup(browser); | ||
await FLOWS.registerPinNewIdentityAuthenticateView(pin, browser); | ||
|
||
const principal = await demoAppView.waitForAuthenticated(); | ||
expect(await demoAppView.whoami()).toBe(principal); | ||
|
||
// default value | ||
const exp = await browser.$("#expiration").getText(); | ||
expect(Number(exp) / (8 * 60 * 60_000_000_000)).toBeCloseTo(1); | ||
}, APPLE_USER_AGENT); | ||
}, 300_000); | ||
|
||
test.skip("Register with PIN then log into client application", async () => { | ||
test("Register with PIN then log into client application", async () => { | ||
await runInBrowser(async (browser: WebdriverIO.Browser) => { | ||
const pin = "123456"; | ||
|
||
await browser.url(II_URL); | ||
await browser.url(`${II_URL}?${ENABLE_PIN_QUERY_PARAM_KEY}`); | ||
const userNumber = await FLOWS.registerPinWelcomeView(browser, pin); | ||
|
||
const demoAppView = new DemoAppView(browser); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes in this PR are mainly reverting #2687