@auth/sveltekit
Version:
Authentication for SvelteKit.
352 lines (351 loc) • 14.5 kB
JavaScript
/**
* :::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);
},
};
}