From 08162cb864a0b71cb196f84230d4d3e8ddf130b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 16 Nov 2024 13:06:33 +0100 Subject: [PATCH] more tweak sveltekit --- .../frameworks-sveltekit/src/lib/client.ts | 59 +++++--- .../frameworks-sveltekit/src/lib/webauthn.ts | 134 ++++++++++-------- 2 files changed, 119 insertions(+), 74 deletions(-) diff --git a/packages/frameworks-sveltekit/src/lib/client.ts b/packages/frameworks-sveltekit/src/lib/client.ts index 42c27abfce..5e708acdc3 100644 --- a/packages/frameworks-sveltekit/src/lib/client.ts +++ b/packages/frameworks-sveltekit/src/lib/client.ts @@ -1,7 +1,7 @@ import { base } from "$app/paths" import type { ProviderId } from "@auth/core/providers" -interface SignInOptions +export interface SignInOptions extends Record { /** @deprecated Use `redirectTo` instead. */ callbackUrl?: string @@ -20,7 +20,7 @@ interface SignInOptions redirect?: Redirect } -interface SignInResponse { +export interface SignInResponse { error: string | undefined code: string | undefined status: number @@ -28,11 +28,17 @@ interface SignInResponse { url: string | null } -interface SignOutParams { - /** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */ +export interface SignOutParams { + /** @deprecated Use `redirectTo` instead. */ callbackUrl?: string + /** + * If you pass `redirect: false`, the page will not reload. + * The session will be deleted, and `useSession` is notified, so any indication about the user will be shown as logged out automatically. + * It can give a very nice experience for the user. + */ + redirectTo?: string /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */ - redirect?: R + redirect?: Redirect } /** Match `inputType` of `new URLSearchParams(inputType)` */ @@ -121,28 +127,49 @@ export async function signIn( } } +export interface SignOutResponse { + url: string +} + /** - * Signs the user out, by removing the session cookie. + * Initiate a signout, by destroying the current session. + * Handles CSRF protection. * - * [Documentation](https://authjs.dev/reference/sveltekit/client#signout) + * @note This method can only be used from Client Components ("use client" or Pages Router). + * For Server Actions, use the `signOut` method imported from the `auth` config. */ -export async function signOut(options?: SignOutParams) { - const { callbackUrl = window.location.href } = options ?? {} - const basePath = base ?? "" - const res = await fetch(`${basePath}/auth/signout`, { +export async function signOut(options?: SignOutParams): Promise +export async function signOut( + options?: SignOutParams +): Promise +export async function signOut( + options?: SignOutParams +): Promise { + const { + redirect = true, + redirectTo = options?.callbackUrl ?? window.location.href, + } = options ?? {} + + const baseUrl = base ?? "" + const res = await fetch(`${baseUrl}/signout`, { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Auth-Return-Redirect": "1", }, body: new URLSearchParams({ - callbackUrl, + callbackUrl: redirectTo, }), }) const data = await res.json() - const url = data.url ?? callbackUrl - window.location.href = url - // If url contains a hash, the browser does not reload the page. We reload manually - if (url.includes("#")) window.location.reload() + if (redirect) { + const url = data.url ?? redirectTo + window.location.href = url + // If url contains a hash, the browser does not reload the page. We reload manually + if (url.includes("#")) window.location.reload() + return + } + + return data } diff --git a/packages/frameworks-sveltekit/src/lib/webauthn.ts b/packages/frameworks-sveltekit/src/lib/webauthn.ts index 9ef4d4ed04..1847ea12eb 100644 --- a/packages/frameworks-sveltekit/src/lib/webauthn.ts +++ b/packages/frameworks-sveltekit/src/lib/webauthn.ts @@ -1,30 +1,36 @@ import { base } from "$app/paths" import { startAuthentication, startRegistration } from "@simplewebauthn/browser" -import type { - BuiltInProviderType, - RedirectableProviderType, -} from "@auth/core/providers" +import type { LoggerInstance } from "@auth/core/types" import type { WebAuthnOptionsResponseBody } from "@auth/core/types" -import type { SignInOptions, SignInAuthorizationParams } from "./client.js" -import type { LiteralUnion } from "./types.js" +import type { ProviderId } from "@auth/core/providers" +import type { + SignInAuthorizationParams, + SignInOptions, + SignInResponse, +} from "./client.js" + +const logger: LoggerInstance = { + debug: console.debug, + error: console.error, + warn: console.warn, +} /** * Fetch webauthn options from server and prompt user for authentication or registration. * Returns either the completed WebAuthn response or an error request. - * - * @param providerId provider ID - * @param options SignInOptions - * @returns WebAuthn response or error */ -async function webAuthnOptions(providerId: string, options?: SignInOptions) { - const baseUrl = `${base}/auth` +async function webAuthnOptions( + providerID: ProviderId, + options?: Omit +) { + const baseUrl = base ?? "" // @ts-expect-error const params = new URLSearchParams(options) const optionsResp = await fetch( - `${baseUrl}/webauthn-options/${providerId}?${params}` + `${baseUrl}/webauthn-options/${providerID}?${params}` ) if (!optionsResp.ok) { return { error: optionsResp } @@ -41,72 +47,84 @@ async function webAuthnOptions(providerId: string, options?: SignInOptions) { } /** - * Client-side method to initiate a webauthn signin flow - * or send the user to the signin page listing all possible providers. - * - * [Documentation](https://authjs.dev/reference/sveltekit/client#signin) + * Initiate a WebAuthn signin flow. + * @see https://authjs.dev/getting-started/authentication/webauthn */ -export async function signIn< - P extends RedirectableProviderType | undefined = undefined, ->( - providerId?: LiteralUnion< - P extends RedirectableProviderType - ? P | BuiltInProviderType - : BuiltInProviderType - >, - options?: SignInOptions, +export async function signIn( + provider?: ProviderId, + options?: SignInOptions, authorizationParams?: SignInAuthorizationParams -) { - const { callbackUrl = window.location.href, redirect = true } = options ?? {} - - // TODO: Support custom providers - const isCredentials = providerId === "credentials" - const isEmail = providerId === "email" - const isWebAuthn = providerId === "webauthn" - const isSupportingReturn = isCredentials || isEmail || isWebAuthn +): Promise +export async function signIn( + provider?: ProviderId, + options?: SignInOptions, + authorizationParams?: SignInAuthorizationParams +): Promise +export async function signIn( + provider?: ProviderId, + options?: SignInOptions, + authorizationParams?: SignInAuthorizationParams +): Promise { + const { callbackUrl, ...rest } = options ?? {} + const { + redirectTo = callbackUrl ?? window.location.href, + redirect = true, + ...signInParams + } = rest - const basePath = base ?? "" - const signInUrl = `${basePath}/auth/${ - isCredentials || isWebAuthn ? "callback" : "signin" - }/${providerId}` + const baseUrl = base ?? "" - const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}` + if (!provider || provider !== "webauthn") { + // TODO: Add docs link with explanation + throw new TypeError( + [ + `Provider id "${provider}" does not refer to a WebAuthn provider.`, + 'Please use `import { signIn } from "@auth/sveltekit/client"` instead.', + ].join("\n") + ) + } - // Execute WebAuthn client flow if needed const webAuthnBody: Record = {} - if (isWebAuthn) { - const { data, error, action } = await webAuthnOptions(providerId, options) - if (error) { - // logger.error(new Error(await error.text())) - return - } - webAuthnBody.data = JSON.stringify(data) - webAuthnBody.action = action + const webAuthnResponse = await webAuthnOptions(provider, signInParams) + if (webAuthnResponse.error) { + logger.error(new Error(await webAuthnResponse.error.text())) + return } + webAuthnBody.data = JSON.stringify(webAuthnResponse.data) + webAuthnBody.action = webAuthnResponse.action - const res = await fetch(_signInUrl, { + const signInUrl = `${baseUrl}/callback/${provider}?${new URLSearchParams(authorizationParams)}` + const res = await fetch(signInUrl, { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-Auth-Return-Redirect": "1", }, - // @ts-ignore body: new URLSearchParams({ - ...options, - callbackUrl, + ...signInParams, ...webAuthnBody, + callbackUrl: redirectTo, }), }) - const data = await res.clone().json() + const data = await res.json() - if (redirect || !isSupportingReturn) { - // TODO: Do not redirect for Credentials and Email providers by default in next major - window.location.href = data.url ?? callbackUrl + if (redirect) { + const url = data.url ?? callbackUrl + window.location.href = url // If url contains a hash, the browser does not reload the page. We reload manually - if (data.url.includes("#")) window.location.reload() + if (url.includes("#")) window.location.reload() return } - return res + const error = new URL(data.url).searchParams.get("error") + const code = new URL(data.url).searchParams.get("code") + + return { + error, + code, + status: res.status, + ok: res.ok, + url: error ? null : data.url, + } as any }