-
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 5 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< | ||
|
@@ -87,6 +98,7 @@ export const registerFlow = async ({ | |
| RateLimitExceeded | ||
| "canceled" | ||
> => { | ||
console.log("in da registerFlow"); | ||
sea-snake marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!registrationAllowed) { | ||
const result = await registerDisabled(); | ||
result satisfies { tag: "canceled" }; | ||
|
@@ -98,24 +110,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 +220,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 +233,7 @@ export const registerFlow = async ({ | |
); | ||
await displayUserNumber({ | ||
userNumber, | ||
marketingIntroSlot: finishSlot, | ||
}); | ||
return { ...result, authnMethod }; | ||
}; | ||
|
@@ -180,6 +252,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 +268,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; | ||
|
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