Skip to content

Commit

Permalink
feat(sveltekit): webauthn provider support (#9924)
Browse files Browse the repository at this point in the history
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Vishal Kashi <dev.vishalkashi@gmail.com>
Co-authored-by: Gustavo Maronato <gustavo.maronato@klaviyo.com>
Co-authored-by: Gustavo Maronato <gustavo@maronato.dev>
  • Loading branch information
5 people committed May 11, 2024
1 parent 89ef33f commit e17cc71
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 29 deletions.
1 change: 1 addition & 0 deletions apps/dev/sveltekit/.gitignore
Expand Up @@ -10,3 +10,4 @@ node_modules
.output
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
tmp-unstorage
3 changes: 1 addition & 2 deletions apps/dev/sveltekit/package.json
Expand Up @@ -21,8 +21,7 @@
"dependencies": {
"@auth/sveltekit": "workspace:*",
"@auth/unstorage-adapter": "workspace:*",
"nodemailer": "^6.9.3",
"unstorage": "^1.10.1"
"unstorage": "^1.10.2"
},
"type": "module"
}
15 changes: 11 additions & 4 deletions apps/dev/sveltekit/src/auth.ts
Expand Up @@ -4,15 +4,21 @@ import Credentials from "@auth/sveltekit/providers/credentials"
import Facebook from "@auth/sveltekit/providers/facebook"
import Discord from "@auth/sveltekit/providers/discord"
import Google from "@auth/sveltekit/providers/google"
import Passkey from "@auth/sveltekit/providers/passkey"
import { createStorage } from "unstorage"
import { UnstorageAdapter } from "@auth/unstorage-adapter"
import fsDriver from "unstorage/drivers/fs"
import { dev } from "$app/environment"

const storage = createStorage({
driver: fsDriver({ base: "./tmp-unstorage" }),
})

const storage = createStorage()
export const { handle, signIn, signOut } = SvelteKitAuth({
debug: true,
debug: dev ? true : false,
adapter: UnstorageAdapter(storage),
session: {
strategy: "jwt",
experimental: {
enableWebAuthn: true,
},
providers: [
Credentials({
Expand All @@ -31,6 +37,7 @@ export const { handle, signIn, signOut } = SvelteKitAuth({
Google,
Facebook,
Discord,
Passkey,
],
theme: {
logo: "https://authjs.dev/img/logo-sm.png",
Expand Down
2 changes: 1 addition & 1 deletion apps/dev/sveltekit/src/routes/signin/+page.server.ts
@@ -1,4 +1,4 @@
import { signIn } from "../../auth"
import { signIn } from "$/auth"
import type { Actions } from "./$types"

export const actions = { default: signIn } satisfies Actions
2 changes: 1 addition & 1 deletion apps/dev/sveltekit/src/routes/signout/+page.server.ts
@@ -1,4 +1,4 @@
import { signOut } from "../../auth"
import { signOut } from "$/auth"
import type { Actions } from "./$types"

export const actions = { default: signOut } satisfies Actions
8 changes: 3 additions & 5 deletions apps/dev/sveltekit/vite.config.js
@@ -1,11 +1,9 @@
import { defineConfig } from "vite"
import { sveltekit } from "@sveltejs/kit/vite"

/** @type {import('vite').UserConfig} */
const config = {
export default defineConfig({
server: {
port: 3000,
},
plugins: [sveltekit()],
}

export default config
})
5 changes: 3 additions & 2 deletions docs/pages/getting-started/providers/passkey.mdx
Expand Up @@ -17,9 +17,10 @@ as any database adapter that plans to support it. Therefore, the WebAuthn provid
is currently only supported in the following framework integration and database adapters.
Support for more frameworks and adapters are coming soon.{" "}

- `next-auth@5.0.0-beta.9` or above
- `next-auth@5.0.0-beta.17` or above
- `@auth/sveltekit@1.0.2` or above
- `@auth/prisma-adapter@1.3.3` or above
- `@prisma/client@5.9.1` or above
- `@prisma/client@5.12.0` or above

<Steps>
### Install peer dependencies
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Expand Up @@ -73,6 +73,8 @@ import type {
WebAuthnProviderType,
} from "./providers/webauthn.js"

export type { WebAuthnOptionsResponseBody } from "./lib/utils/webauthn-utils.js"
export type { AuthConfig } from "./index.js"
export type { LoggerInstance }
export type Awaitable<T> = T | PromiseLike<T>
export type Awaited<T> = T extends Promise<infer U> ? U : T
Expand Down
18 changes: 18 additions & 0 deletions packages/frameworks-sveltekit/package.json
Expand Up @@ -50,9 +50,23 @@
"set-cookie-parser": "^2.6.0"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3",
"@sveltejs/kit": "^1.0.0 || ^2.0.0",
"nodemailer": "^6.6.5",
"svelte": "^3.54.0 || ^4.0.0 || ^5"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
},
"type": "module",
"types": "./dist/index.d.ts",
"files": [
Expand All @@ -68,6 +82,10 @@
"types": "./dist/client.d.ts",
"import": "./dist/client.js"
},
"./webauthn": {
"types": "./dist/webauthn.d.ts",
"import": "./dist/webauthn.js"
},
"./components": {
"types": "./dist/components/index.d.ts",
"svelte": "./dist/components/index.js"
Expand Down
10 changes: 6 additions & 4 deletions packages/frameworks-sveltekit/src/lib/client.ts
@@ -1,12 +1,14 @@
import { base } from "$app/paths"
import type {
BuiltInProviderType,
RedirectableProviderType,
} from "@auth/core/providers"
import { base } from "$app/paths"
import type { LiteralUnion } from "./types.js"

type LiteralUnion<T extends U, U = string> = T | (U & Record<never, never>)

interface SignInOptions extends Record<string, unknown> {
/*
* @internal
*/
export interface SignInOptions extends Record<string, unknown> {
/**
* Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/frameworks-sveltekit/src/lib/types.ts
Expand Up @@ -2,6 +2,10 @@ import type { AuthConfig } from "@auth/core"
import type { BuiltInProviderType } from "@auth/core/providers"
import type { Session } from "@auth/core/types"

export type LiteralUnion<T extends U, U = string> =
| T
| (U & Record<never, never>)

/** Configure the {@link SvelteKitAuth} method. */
export interface SvelteKitAuthConfig extends Omit<AuthConfig, "raw"> {}

Expand Down
118 changes: 118 additions & 0 deletions packages/frameworks-sveltekit/src/lib/webauthn.ts
@@ -0,0 +1,118 @@
import { base } from "$app/paths"
import { startAuthentication, startRegistration } from "@simplewebauthn/browser"

import type {
BuiltInProviderType,
RedirectableProviderType,
} from "@auth/core/providers"
import type { WebAuthnOptionsResponseBody } from "@auth/core/types"
import type { SignInOptions, SignInAuthorizationParams } from "./client.js"
import type { LiteralUnion } from "./types.js"

/**
* 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/`

// @ts-expect-error
const params = new URLSearchParams(options)

const optionsResp = await fetch(
`${baseUrl}/webauthn-options/${providerId}?${params}`
)
if (!optionsResp.ok) {
return { error: optionsResp }
}
const optionsData: WebAuthnOptionsResponseBody = await optionsResp.json()

if (optionsData.action === "authenticate") {
const webAuthnResponse = await startAuthentication(optionsData.options)
return { data: webAuthnResponse, action: "authenticate" }
} else {
const webAuthnResponse = await startRegistration(optionsData.options)
return { data: webAuthnResponse, action: "register" }
}
}

/**
* Client-side method to initiate a webauthn signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://authjs.dev/reference/sveltekit/client#signin)
*/
export async function signIn<
P extends RedirectableProviderType | undefined = undefined,
>(
providerId?: LiteralUnion<
P extends RedirectableProviderType
? P | BuiltInProviderType
: BuiltInProviderType
>,
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

const basePath = base ?? ""
const signInUrl = `${basePath}/auth/${
isCredentials || isWebAuthn ? "callback" : "signin"
}/${providerId}`

const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`

// Execute WebAuthn client flow if needed
const webAuthnBody: Record<string, unknown> = {}
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
}

// TODO: Remove this since Sveltekit offers the CSRF protection via origin check
const csrfTokenResponse = await fetch(`${basePath}/auth/csrf`)
const { csrfToken } = await csrfTokenResponse.json()

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,
csrfToken,
callbackUrl,
...webAuthnBody,
}),
})

const data = await res.clone().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 url contains a hash, the browser does not reload the page. We reload manually
if (data.url.includes("#")) window.location.reload()
return
}

return res
}
2 changes: 1 addition & 1 deletion packages/next-auth/src/webauthn.ts
Expand Up @@ -3,7 +3,7 @@ import { startAuthentication, startRegistration } from "@simplewebauthn/browser"
import { getCsrfToken, getProviders, __NEXTAUTH } from "./react.js"

import type { LoggerInstance } from "@auth/core/types"
import type { WebAuthnOptionsResponseBody } from "@auth/core/lib/utils/webauthn-utils"
import type { WebAuthnOptionsResponseBody } from "@auth/core/types"
import type {
BuiltInProviderType,
RedirectableProviderType,
Expand Down

0 comments on commit e17cc71

Please sign in to comment.