UNPKG

@auth/sveltekit

Version:

Authentication for SvelteKit.

352 lines (351 loc) 14.5 kB
/** * :::warning * `@auth/sveltekit` is currently experimental. The API _might_ change. * ::: * * SvelteKit Auth is the official SvelteKit integration for Auth.js. * It provides a simple way to add authentication to your SvelteKit app in a few lines of code. * * ## Installation * * ```bash npm2yarn * npm install @auth/sveltekit * ``` * * ## Usage * * ```ts title="src/auth.ts" * * import { SvelteKitAuth } from "@auth/sveltekit" * import GitHub from "@auth/sveltekit/providers/github" * * export const { handle, signIn, signOut } = SvelteKitAuth({ * providers: [GitHub], * }) * ``` * * ### Lazy initialization * `@auth/sveltekit` supports lazy initialization where you can read the `event` object to lazily set the configuration. This is especially useful when you have to get the environment variables from `event.platform` for platforms like Cloudflare Workers. * * ```ts title="src/auth.ts" * import { SvelteKitAuth } from "@auth/sveltekit" * import GitHub from "@auth/sveltekit/providers/github" * * export const { handle, signIn, signOut } = SvelteKitAuth(async (event) => { * const authOptions = { * providers: [ * GitHub({ * clientId: event.platform.env.AUTH_GITHUB_ID, * clientSecret: event.platform.env.AUTH_GITHUB_SECRET * }) * ], * secret: event.platform.env.AUTH_SECRET, * trustHost: true * } * return authOptions * }) * ``` * * Re-export the handle in `src/hooks.server.ts`: * ```ts title="src/hooks.server.ts" * export { handle } from "./auth" * ``` * * Remember to set the `AUTH_SECRET` [environment variable](https://kit.svelte.dev/docs/modules#$env-dynamic-private). This should be a minimum of 32 characters, random string. On UNIX systems you can use `openssl rand -hex 32` or check out `https://generate-secret.vercel.app/32`. * * When deploying your app outside Vercel, set the `AUTH_TRUST_HOST` variable to `true` for other hosting providers like Cloudflare Pages or Netlify. * * The callback URL used by the [providers](https://authjs.dev/getting-started/providers) must be set to the following, unless you override {@link SvelteKitAuthConfig.basePath}: * * ``` * [origin]/auth/callback/[provider] * ``` * * ## Signing in and Signing out * * ### Server-side * * `<SignIn />` and `<SignOut />` are components that `@auth/sveltekit` provides out of the box - they handle the sign-in/signout flow, and can be used as-is as a starting point or customized for your own components. This is an example of how to use the `SignIn` and `SignOut` components to login and logout using SvelteKit's server-side form actions. Another example is available on [our svelte-auth-example repo](https://github.com/nextauthjs/sveltekit-auth-example). * * You will need two things to make this work: * * 1. Using the components in your SvelteKit app's frontend (for instance `src/routes/+page.svelte`) * 2. Add the required `page.server.ts` at `/signin` (for `SignIn`) and `/signout` (for `SignOut`) to handle the form actions * * ```ts title="src/routes/+page.svelte" * <script> * import { SignIn, SignOut } from "@auth/sveltekit/components" * import { page } from "$app/stores" * </script> * * <h1>SvelteKit Auth Example</h1> * <div> * {#if $page.data.session} * {#if $page.data.session.user?.image} * <img * src={$page.data.session.user.image} * class="avatar" * alt="User Avatar" * /> * {/if} * <span class="signedInText"> * <small>Signed in as</small><br /> * <strong>{$page.data.session.user?.name ?? "User"}</strong> * </span> * <SignOut> * <div slot="submitButton" class="buttonPrimary">Sign out</div> * </SignOut> * {:else} * <span class="notSignedInText">You are not signed in</span> * <SignIn> * <div slot="submitButton" class="buttonPrimary">Sign in</div> * </SignIn> * <SignIn provider="facebook"/> * {/if} * </div> * ``` * * To set up the form actions, we need to define the files in `src/routes`: * * ```ts title="src/routes/signin/+page.server.ts" * import { signIn } from "../../auth" * import type { Actions } from "./$types" * export const actions: Actions = { default: signIn } * ``` * * ```ts title="src/routes/signout/+page.server.ts" * import { signOut } from "../../auth" * import type { Actions } from "./$types" * export const actions: Actions = { default: signOut } * ``` * * These routes are customizeable with the `signInPage` and `signOutPage` props on the respective comopnents. * * ### Client-Side * * We also export two methods from `@auth/sveltekit/client` in order to do client-side sign-in and sign-out actions. * * ```ts title="src/routes/+page.svelte" * <script> * import { signIn, signOut } from "@auth/sveltekit/client" * let password * </script> * * <nav> * <p> * These actions are all using the methods exported from * <code>@auth/sveltekit/client</code> * </p> * <div class="actions"> * <div class="wrapper-form"> * <button on:click={() => signIn("github")}>Sign In with GitHub</button> * </div> * <div class="wrapper-form"> * <button on:click={() => signIn("discord")}>Sign In with Discord</button> * </div> * <div class="wrapper-form"> * <div class="input-wrapper"> * <label for="password">Password</label> * <input * bind:value={password} * type="password" * id="password" * name="password" * required * /> * </div> * <button on:click={() => signIn("credentials", { password })}> * Sign In with Credentials * </button> * <button on:click={() => signOut()}> * Sign Out * </button> * </div> * </div> * </nav> * ``` * * ## Managing the session * * The above example checks for a session available in `$page.data.session`, however that needs to be set by us somewhere. * If you want this data to be available to all your routes you can add this to `src/routes/+layout.server.ts`. * The following code sets the session data in the `$page` store to be available to all routes. * * ```ts * import type { LayoutServerLoad } from './$types'; * * export const load: LayoutServerLoad = async (event) => { * return { * session: await event.locals.auth() * }; * }; * ``` * * What you return in the function `LayoutServerLoad` will be available inside the `$page` store, in the `data` property: `$page.data`. * In this case we return an object with the `session` property which is what we are accessing in the other code paths. * * ## Handling authorization * * In SvelteKit there are a few ways you could protect routes from unauthenticated users. * * ### Per component * * The simplest case is protecting a single page, in which case you should put the logic in the `+page.server.ts` file. * Notice in this case that you could also `await event.parent` and grab the session from there, however this implementation works even if you haven't done the above in your root `+layout.server.ts` * * ```ts * import { redirect } from '@sveltejs/kit'; * import type { PageServerLoad } from './$types'; * * export const load: PageServerLoad = async (event) => { * const session = await event.locals.auth(); * if (!session?.user) throw redirect(303, '/auth'); * return {}; * }; * ``` * * :::danger * Make sure to ALWAYS grab the session information from the parent instead of using the store in the case of a `PageLoad`. * Not doing so can lead to users being able to incorrectly access protected information in the case the `+layout.server.ts` does not run for that page load. * For more information on SvelteKit's `load` functionality behaviour and its implications on authentication, see this [SvelteKit docs section](https://kit.svelte.dev/docs/load#implications-for-authentication). * ::: * * You should NOT put authorization logic in a `+layout.server.ts` as the logic is not guaranteed to propagate to leafs in the tree. * Prefer to manually protect each route through the `+page.server.ts` file to avoid mistakes. * It is possible to force the layout file to run the load function on all routes, however that relies certain behaviours that can change and are not easily checked. * For more information about these caveats make sure to read this issue in the SvelteKit repository: https://github.com/sveltejs/kit/issues/6315 * * ### Per path * * Another method that's possible for handling authorization is by restricting certain URIs from being available. * For many projects this is better because: * - This automatically protects actions and api routes in those URIs * - No code duplication between components * - Very easy to modify * * The way to handle authorization through the URI is to override your handle hook. * The handle hook, returned from `SvelteKitAuth` in your `src/auth.ts`, is a function that is designed to receive ALL requests sent to your SvelteKit webapp. * You should export it from `src/auth.ts` and import it in your `src/hooks.server.ts`. * To use multiple handles in your `hooks.server.ts`, we can use SvelteKit's `sequence` to execute all of them in series. * * ```ts title="src/auth.ts" * import { SvelteKitAuth } from '@auth/sveltekit'; * import GitHub from '@auth/sveltekit/providers/github'; * * export const { handle, signIn, signOut } = SvelteKitAuth({ * providers: [GitHub] * }), * ``` * * ```ts title="src/hooks.server.ts" * import { redirect, type Handle } from '@sveltejs/kit'; * import { handle as authenticationHandle } from './auth'; * import { sequence } from '@sveltejs/kit/hooks'; * * async function authorizationHandle({ event, resolve }) { * // Protect any routes under /authenticated * if (event.url.pathname.startsWith('/authenticated')) { * const session = await event.locals.auth(); * if (!session) { * // Redirect to the signin page * throw redirect(303, '/auth/signin'); * } * } * * // If the request is still here, just proceed as normally * return resolve(event); * } * * // First handle authentication, then authorization * // Each function acts as a middleware, receiving the request handle * // And returning a handle which gets passed to the next function * export const handle: Handle = sequence(authenticationHandle, authorizationHandle) * ``` * * :::info * Learn more about SvelteKit's handle hooks and sequence [here](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks-sequence). * ::: * * Now any routes under `/authenticated` will be transparently protected by the handle hook. * You may add more middleware-like functions to the sequence and also implement more complex authorization business logic inside this file. * This can also be used along with the component-based approach in case you need a specific page to be protected and doing it by URI could be faulty. * * ## Notes * * - If you build your SvelteKit application with `prerender` enabled, pages which have an anchor tag to the default signin page (i.e. `<a href="/auth/signin" ...`) will have trouble building. Please use the [builtin functions or components](https://authjs.dev/getting-started/session-management/login?framework=sveltekit) to sign in or out instead. * * :::info * Learn more about `@auth/sveltekit` [here](https://vercel.com/blog/announcing-sveltekit-auth). * ::: * * @module @auth/sveltekit */ import "@sveltejs/kit"; import { env } from "$env/dynamic/private"; import { setEnvDefaults } from "./env"; import { auth, signIn, signOut } from "./actions"; import { Auth, isAuthAction, customFetch } from "@auth/core"; import { building } from "$app/environment"; export { customFetch }; export { AuthError, CredentialsSignin } from "@auth/core/errors"; const authorizationParamsPrefix = "authorizationParams-"; /** * The main entry point to `@auth/sveltekit` * @see https://sveltekit.authjs.dev */ export function SvelteKitAuth(config) { return { signIn: async (event) => { if (building) return; const { request } = event; const _config = typeof config === "object" ? config : await config(event); setEnvDefaults(env, _config); const formData = await request.formData(); const { providerId: provider, ...options } = Object.fromEntries(formData); // get the authorization params from the options prefixed with `authorizationParams-` const authorizationParams = {}; const _options = {}; for (const key in options) { if (key.startsWith(authorizationParamsPrefix)) { authorizationParams[key.slice(authorizationParamsPrefix.length)] = options[key]; } else { _options[key] = options[key]; } } await signIn(provider, _options, authorizationParams, _config, event); }, signOut: async (event) => { if (building) return; const _config = typeof config === "object" ? config : await config(event); setEnvDefaults(env, _config); const options = Object.fromEntries(await event.request.formData()); await signOut(options, _config, event); }, async handle({ event, resolve }) { if (building) { event.locals.auth ??= async () => null; event.locals.getSession ??= event.locals.auth; return resolve(event); } const _config = typeof config === "object" ? config : await config(event); setEnvDefaults(env, _config); const { url, request } = event; event.locals.auth ??= () => auth(event, _config); event.locals.getSession ??= event.locals.auth; const action = url.pathname .slice( // @ts-expect-error - basePath is defined in setEnvDefaults _config.basePath.length + 1) .split("/")[0]; if (isAuthAction(action) && url.pathname.startsWith(_config.basePath + "/")) { return Auth(request, _config); } return resolve(event); }, }; }