-
Notifications
You must be signed in to change notification settings - Fork 212
@W-20224082 - [Webauthn] Create passkey in browser and register in SLAS #3584
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
Changes from 32 commits
33732bd
49d41c6
016ac69
ba2e2e8
2d8e140
d626a93
7a4b57c
028e560
9cab5e2
319d109
240a7b8
13704ab
6c8f205
152e35d
dd47c53
f80a2bd
a180a8b
674b624
608910a
4e56ffe
405ee0f
f4e8321
83de486
4c53b55
6b45907
47d5556
8399163
068b055
f92b0b9
f9854b7
9e3d05f
830c9ce
04283a3
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 |
|---|---|---|
|
|
@@ -52,6 +52,8 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => { | |
| const config = getConfig() | ||
| const webauthnConfig = config.app.login.passkey | ||
| const authorizeWebauthnRegistration = useAuthHelper(AuthHelpers.AuthorizeWebauthnRegistration) | ||
| const startWebauthnUserRegistration = useAuthHelper(AuthHelpers.StartWebauthnUserRegistration) | ||
| const finishWebauthnUserRegistration = useAuthHelper(AuthHelpers.FinishWebauthnUserRegistration) | ||
|
|
||
| const handleRegisterPasskey = async () => { | ||
| setIsLoading(true) | ||
|
|
@@ -82,9 +84,128 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Convert base64url string to ArrayBuffer | ||
| */ | ||
| const base64UrlToArrayBuffer = (base64Url) => { | ||
| const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/') | ||
| const binaryString = atob(base64) | ||
| const bytes = new Uint8Array(binaryString.length) | ||
| for (let i = 0; i < binaryString.length; i++) { | ||
| bytes[i] = binaryString.charCodeAt(i) | ||
| } | ||
| return bytes.buffer | ||
| } | ||
|
|
||
| /** | ||
| * Convert ArrayBuffer to base64url string | ||
| */ | ||
| const arrayBufferToBase64Url = (buffer) => { | ||
| const bytes = new Uint8Array(buffer) | ||
| let binary = '' | ||
| for (let i = 0; i < bytes.length; i++) { | ||
| binary += String.fromCharCode(bytes[i]) | ||
| } | ||
| const base64 = btoa(binary) | ||
| return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') | ||
| } | ||
|
|
||
| const handleOtpVerification = async (code) => { | ||
| // TODO: Implement OTP verification | ||
| return {success: true} | ||
| setIsLoading(true) | ||
| setError(null) | ||
|
|
||
| try { | ||
| // Step 1: Start WebAuthn registration | ||
| const response = await startWebauthnUserRegistration.mutateAsync({ | ||
| user_id: customer.email, | ||
| pwd_action_token: code, | ||
| ...(passkeyNickname && {nick_name: passkeyNickname}) | ||
| }) | ||
|
|
||
| // Step 2: Convert response to WebAuthn PublicKeyCredentialCreationOptions format | ||
| const publicKey = { | ||
| challenge: base64UrlToArrayBuffer(response.challenge), | ||
| rp: { | ||
| name: response.rp.name, | ||
| id: response.rp.id | ||
| }, | ||
| user: { | ||
| ...response.user, | ||
| id: base64UrlToArrayBuffer(response.user.id) | ||
| }, | ||
| pubKeyCredParams: response.pubKeyCredParams || [], | ||
| authenticatorSelection: response.authenticatorSelection, | ||
| timeout: response.timeout, | ||
| attestation: response.attestation || 'none' | ||
| } | ||
yunakim714 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Step 3: Call navigator.credentials.create() | ||
| if (!navigator.credentials || !navigator.credentials.create) { | ||
| throw new Error('WebAuthn API not available in this browser') | ||
| } | ||
|
|
||
| // navigator.credentials.create() will show a browser/system prompt | ||
| // This may appear to hang if the user doesn't interact with the prompt | ||
| let credential | ||
| try { | ||
| credential = await navigator.credentials.create({ | ||
| publicKey | ||
| }) | ||
| } catch (createError) { | ||
| // Handle user cancellation or other errors from the WebAuthn API | ||
| if (createError.name === 'NotAllowedError' || createError.name === 'AbortError') { | ||
|
Collaborator
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. should we include a comment on when each of these errors are thrown?
Collaborator
Author
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 will be addressed in the error handling ticket - will note in the description of the new work item! |
||
| throw new Error('Passkey registration was cancelled or timed out') | ||
| } | ||
| throw createError | ||
| } | ||
|
|
||
| if (!credential) { | ||
| throw new Error('Failed to create credential: user cancelled or operation failed') | ||
| } | ||
|
|
||
| // Step 4: Convert credential to JSON format | ||
| const clientExtensionResults = credential.getClientExtensionResults?.() || {} | ||
| const credentialJson = { | ||
| type: credential.type, | ||
| id: credential.id, | ||
| rawId: arrayBufferToBase64Url(credential.rawId), | ||
| response: { | ||
| attestationObject: arrayBufferToBase64Url( | ||
| credential.response.attestationObject | ||
| ), | ||
| clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON) | ||
| }, | ||
| ...(Object.keys(clientExtensionResults).length > 0 && {clientExtensionResults}) | ||
| } | ||
yunakim714 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Step 5: Finish WebAuthn registration | ||
| await finishWebauthnUserRegistration.mutateAsync({ | ||
| username: customer.email, | ||
| credential: credentialJson, | ||
| pwd_action_token: code | ||
| }) | ||
|
|
||
| // Step 6: Close OTP modal and main modal on success | ||
| setIsOtpAuthOpen(false) | ||
| onClose() | ||
|
|
||
| return {success: true} | ||
| } catch (err) { | ||
| const errorMessage = | ||
| err.message || | ||
| formatMessage({ | ||
| id: 'passkey_registration.modal.error.registration_failed', | ||
| defaultMessage: 'Failed to register passkey' | ||
| }) | ||
|
Comment on lines
+175
to
+180
Collaborator
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. should we always return the localized error message and never the api error mesage? or map error messages from the API to a localized message similar to what we did here: https://salesforce.quip.com/97bPANYv5D2U
Collaborator
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. Thinking through this again, we will definitely need a localized error for some errors like when the How about we keep the error handling generic and create a separate story for error handling for registration and login to make sure we handle all the cases.
Collaborator
Author
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. Sounds good - Created the ticket https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE00002TfC8TYAV/view |
||
|
|
||
| // Return error result for OTP component to display | ||
| return { | ||
| success: false, | ||
| error: errorMessage | ||
| } | ||
| } finally { | ||
| setIsLoading(false) | ||
| } | ||
| } | ||
|
|
||
| const resetState = () => { | ||
|
|
||
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.
Should clear the OTP input when shopper clicks Resend Code