Skip to content
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

arePasskeysSupported #23

Open
joshua1 opened this issue Nov 17, 2024 · 8 comments
Open

arePasskeysSupported #23

joshua1 opened this issue Nov 17, 2024 · 8 comments

Comments

@joshua1
Copy link

joshua1 commented Nov 17, 2024

I see this in the docs to check if passkeys are supported in a browser but cant seem to find the right path to import it from. did something change ? I also tried isPassKeySupport but cant import it too. What is the right call to perform this check ?

@joshua1
Copy link
Author

joshua1 commented Nov 17, 2024

Looking through the code, i see a number of the capabilities in the default Passlock class, that are not present in the Passlock class in Sveltekit/superforms (i am using sveltekit) , The Passlock class in superforms should be an extension of the base Passlock class so that those capabilities are available to sveltekit users as it is to others .

@joshua1
Copy link
Author

joshua1 commented Nov 18, 2024

I had it like this in my project and was able to access and use the base Passlock class methods

import { PUBLIC_PASSLOCK_CLIENT_ID, PUBLIC_PASSLOCK_ENDPOINT, PUBLIC_PASSLOCK_TENANCY_ID } from '$env/static/public'

import type { Email, Options, PasslockProps, Principal, ResendEmail, VerifyEmail, VerifyEmailData } from '@passlock/sveltekit'
import { Passlock as Client, ErrorCode, PasslockError } from '@passlock/sveltekit'
import { get } from 'svelte/store'
import type { SuperForm } from 'sveltekit-superforms'


export type RegistrationData = {
      email: string
      givenName?: string
      familyName?: string
      token?: string
      authType: 'apple' | 'google' | 'email' | 'passkey'
      verifyEmail?: 'link' | 'code'
}

export type LoginData = {
      email?: string
      token?: string
      authType: 'apple' | 'google' | 'email' | 'passkey'
}

export type SuperformData<T extends Record<string, unknown>> = {
      cancel: () => void
      formData: FormData
      form: SuperForm<T>
      verifyEmail?: VerifyEmail
}

export class Passlock extends Client {

      constructor(config: PasslockProps) {
            super(config)
      }

      // readonly preConnect = async () => {
      //       await this.preConnect()
      // };

      readonly register = async <T extends RegistrationData>(options: SuperformData<T>) => {
            const { cancel, formData, form, verifyEmail } = options
            const { email, givenName, familyName, token, authType }: RegistrationData = get(form.form)

            if (token && authType) {
                  // a bit hacky but basically the Google button sets the fields on the superform,
                  // who's data is not necessarily posted to the backend unless we use a hidden
                  // form field. We're basically duplicating the role of a hidden field here by
                  // adding the token and authType to the request
                  formData.set('token', token)
                  formData.set('authType', authType)
            } else if (!token && authType === 'passkey') {
                  const principal = await this.registerPasskey({
                        email,
                        ...(givenName ? { givenName } : {}),
                        ...(familyName ? { familyName } : {}),
                        verifyEmail
                  })

                  if (PasslockError.isError(principal) && principal.code === ErrorCode.Duplicate) {
                        // detail will tell the user how to login (passkey or google)
                        const error = principal.detail
                              ? `${principal.message}. ${principal.detail}`
                              : principal.message
                        form.errors.update((errors) => ({ ...errors, email: [error] }))

                        cancel()
                  } else if (PasslockError.isError(principal)) {
                        console.error(principal.message)

                        // set a form level error
                        form.errors.update((errors) => {
                              const _errors = [...(errors._errors ?? []), 'Sorry something went wrong']
                              return { ..._errors, _errors }
                        })

                        cancel()
                  } else if (!Client.isUserPrincipal(principal)) {
                        console.error('No user returned by Passlock')

                        // set a form level error
                        form.errors.update((errors) => {
                              const _errors = [...(errors._errors ?? []), 'Sorry something went wrong']
                              return { ..._errors, _errors }
                        })

                        cancel()
                  } else {
                        // append the passlock token to the form request
                        formData.set('authType', principal.authType)
                        formData.set('token', principal.token)
                        if (verifyEmail) formData.set('verifyEmail', verifyEmail.method)
                  }
            }
      };

      readonly login = async <T extends LoginData>(options: SuperformData<T>) => {
            const { cancel, formData, form } = options
            const { email, token, authType } = get(form.form)

            if (token && authType) {
                  formData.set('token', token)
                  formData.set('authType', authType)
            } else if (!token && authType === 'passkey') {
                  const principal = await this.authenticatePasskey({
                        email,
                        userVerification: 'discouraged'
                  })

                  if (PasslockError.isError(principal) && principal.code === ErrorCode.NotFound) {
                        // detail will tell the user how to login (passkey or google)
                        const error = principal.detail
                              ? `${principal.message}. ${principal.detail}`
                              : principal.message
                        form.errors.update((errors) => ({ ...errors, email: [error] }))
                        cancel()
                  } else if (PasslockError.isError(principal)) {
                        form.message.set(principal.message)
                        cancel()
                  } else if (!Client.isUserPrincipal(principal)) {
                        console.error('No user returned from Passlock')
                        form.message.set('Sorry, something went wrong')
                        cancel()
                  } else {
                        form.form.update((old) => ({ ...old, email: principal.email }))
                        // append the passlock token to the form request
                        formData.set('authType', principal.authType)
                        formData.set('token', principal.token)
                  }
            }
      };

      readonly verifyEmail = async <T extends VerifyEmailData>(options: SuperformData<T>) => {
            const { cancel, formData, form } = options
            const { code } = get(form.form)

            if (code.length >= 6) {
                  const principal = await this.verifyEmailCode({ code })

                  if (PasslockError.isError(principal)) {
                        form.errors.update((old) => ({ ...old, code: [principal.message] }))
                        cancel()
                  } else {
                        formData.set('token', principal.jti)
                  }
            } else {
                  form.errors.update((old) => ({ ...old, code: ['Please enter your code'] }))
                  cancel()
            }
      };

      readonly autoVerifyEmail = async <T extends VerifyEmailData>(form: SuperForm<T>) => {
            if (await this.getSessionToken('passkey')) {
                  form.submit()
            }
      };

      readonly resendEmail = async (options: ResendEmail) => {
            await this.resendVerificationEmail(options)
      };

}

export const updateForm =
      <T extends Record<string, unknown>>(form: SuperForm<T>, onComplete?: () => Promise<void>) =>
            (event: CustomEvent<Principal>) => {
                  form.form.update((old) => ({
                        ...old,
                        email: event.detail.email,
                        ...(event.detail.givenName ? { givenName: event.detail.givenName } : {}),
                        ...(event.detail.familyName ? { familyName: event.detail.familyName } : {}),
                        token: event.detail.jti,
                        authType: event.detail.authType
                  }))

                  if (typeof onComplete === 'function') {
                        onComplete()
                  }
            }

//export { getLocalEmail, saveEmailLocally }


export const passlock = new Passlock({
      tenancyId: PUBLIC_PASSLOCK_TENANCY_ID,
      clientId: PUBLIC_PASSLOCK_CLIENT_ID,
      endpoint: PUBLIC_PASSLOCK_ENDPOINT
})

i removed the private readonly passlock property , extended the base Passlock class and called super in the constructor

@joshua1
Copy link
Author

joshua1 commented Nov 20, 2024

@thobson ^^

@thobson
Copy link
Contributor

thobson commented Nov 20, 2024 via email

@shiftlabs1
Copy link

shiftlabs1 commented Nov 22, 2024

Additionally, in my login schema , i added a field isEmailVerified , and made this slight change in the onSubmit method of Seveltekit-superforms as well as populating that field once a prinicpal is retrieved using emailVerified field , back in onSubmit , i can decide to call resendEmail and redirect the user to the email verification code and link . with all of this, it seems like the sveltekit-superforms bit should be in user land or should be made maleable with config .
The use case here is to want users to actually verify their email even if , they have valid passkeys . What do you think ?

		validators: valibotClient(loginFormSchema),
		onSubmit: async ({ formData, cancel }) => {
			formData.set('returnUrl', $page.url.pathname)
			await passlock.login({ form, formData, cancel })
			if (formData.get('isEmailVerified') !== String(true)) {
				setHomeSheet(SheetTypes.SignUpCode)
				cancel()
			}
		}

and in the login call

		const { cancel, formData, form } = options
		const { email, token, authType } = get(form.form)

		if (token && authType) {
			formData.set('token', token)
			formData.set('authType', authType)
		} else if (!token && authType === 'passkey') {
			const principal = await this.authenticatePasskey({
				email,
				userVerification: 'discouraged'
			})

			switch (true) {
				case PasslockError.isError(principal) && principal.code === ErrorCode.NotFound:
					// detail will tell the user how to login (passkey or google)
					const error = principal.detail
						? `${principal.message}. ${principal.detail}`
						: principal.message
					console.log('passlock error', error)
					form.errors.update((errors) => ({ ...errors, email: [error] }))
					cancel()
					break
				case PasslockError.isError(principal):
					form.message.set(principal.message)
					cancel()
					break
				case !Client.isUserPrincipal(principal):
					console.error('No user returned from Passlock')
					form.message.set('Sorry, something went wrong')
					cancel()
					break
				case !PasslockError.isError(principal) && !principal?.emailVerified:
					await this.resendEmail({
						userId: principal.sub,
						method: 'code'
					})
					formData.set('isEmailVerified', String(false))
					//cancel()
					break
				default:
					form.form.update((old) => ({ ...old, email: principal.email }))
					// append the passlock token to the form request
					formData.set('authType', principal.authType)
					formData.set('token', principal.token)
					formData.set('isEmailVerified', String(principal.emailVerified))
					break
			}
		}
	}```

@thobson
Copy link
Contributor

thobson commented Nov 26, 2024

@shiftlabs1 can I ask you to create a separate issue for your use case? If I’ve understood you correctly you want the ability to verify a users email not just during the initial registration but also during login?

BTW I'm going to expand the server side API and include a sendVerificationEmail type endpoint which might work for your use case.

@thobson
Copy link
Contributor

thobson commented Nov 27, 2024

@joshua1 For now, I've exposed isPasskeySupport = (): Promise<boolean> on the Superforms/Passlock instance. Please use 0.9.31.

@shiftlabs1
Copy link

shiftlabs1 commented Nov 29, 2024

@shiftlabs1 can I ask you to create a separate issue for your use case? If I’ve understood you correctly you want the ability to verify a users email not just during the initial registration but also during login?

BTW I'm going to expand the server side API and include a sendVerificationEmail type endpoint which might work for your use case.

Sorry @thobson . Both accounts happened to be mine (Shiftlabs1 and joshua1) . The sendVerificationEmail will be helpful. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants