A SvelteKit library for seamless server-side authentication using Firebase Authentication.
Partially inspired by next-firebase-auth, check out its discussion of when (not) to use this package.
This is a new library so bugs are expected, please open a new issue if you encounter any or (better yet) open a new PR. Please be patient as this is a one man team here :)
This guide will assume your app uses typescript
npm i sveltekit-fireauth firebase firebase-admin
app.d.ts
import type { FirebaseAuth } from 'sveltekit-fireauth/server'
declare global {
namespace App {
interface Locals {
auth: FirebaseAuth
}
}
}
export {}
hooks.server.ts
import type { Handle } from '@sveltejs/kit'
import { createAuthHandle } from 'sveltekit-fireauth/server'
import { FIREBASE_SERVICE_ACCOUNT_KEY } from '$env/static/private'
export const handle: Handle = createAuthHandle({
// Your web app's Firebase configuration
firebaseConfig: {
apiKey: '',
authDomain: '',
projectId: '',
storageBucket: '',
messagingSenderId: '',
appId: '',
},
// Optional. Just set the FIREBASE_SERVICE_ACCOUNT_KEY environment variable and the library will pick it up
serviceAccountKey: FIREBASE_SERVICE_ACCOUNT_KEY,
// Optional. Refresh token cookie expire time, default 30 days
refreshExpireTime: 60 * 60 * 24 * 30,
})
You may want to use the sequence helper function to set up multiple handle hooks, especially if you are going to use this library's other handle hooks.
import { loginWithCredentials, signupWithCredentials, signOut } from 'sveltekit-fireauth/server'
export const actions = {
login: async (event) => {
// Get the email and password from the form
try {
await loginWithCredentials({ event, email, password })
} catch (e) {
throw error(401, { message: 'Unauthorized' })
}
throw redirect(303, '/protected')
},
signup: async (event) => {
// Get the email and password from the form
try {
await signupWithCredentials({ event, email, password })
} catch (e) {
throw error(401, { message: 'Account cannot be created' })
}
throw redirect(303, '/protected')
},
logout: async ({ cookies }) => {
return signOut({ cookies, redirectRoute: '/login' })
},
}
If you prefer so, this could be done using request handlers as well.
// +layout.server.ts
import { verifySession } from 'sveltekit-fireauth/server'
export const load = (event) => ({
session: verifySession(event),
})
<!-- +layout.svelte -->
<script lang="ts">
export let data
$: ({ session } = data)
</script>
<p>Logged in as user: {session.uid}</p>
You can protect a page using the onlyAuthenticatedLoad
function inside any page's +page.server.ts
file. For example:
// /protected/+page.server.ts
import { onlyAuthenticatedLoad } from 'sveltekit-fireauth/server'
export const load = onlyAuthenticatedLoad({
redirectRoute: '/login',
load: () => {
// regular load function here
},
})
Passing a load function is optional, in case a particular page does not load anything from the server.
// /protected/+page.server.ts
import { onlyAuthenticatedLoad } from 'sveltekit-fireauth/server'
export const load = onlyAuthenticatedLoad({
redirectRoute: '/login',
})
This approach gives you the flexibility to handle authentication on a per page basis and without introducing waterfalls. This also seems to be the recommended approach to handle authentication in SvelteKit, at least as a TLDR from this discussion.
However, I'm aware this approach also introduces some overhead, having to call onlyAuthenticatedLoad
on every page you want to protect. So this library also includes a createProtectedRoutesHandle
for you to use in your hooks.server.ts
.
import { createAuthHandle } from 'sveltekit-fireauth/server'
const protectedRoutesHandle: Handle = createProtectedRoutesHandle({
baseRoute: '/protected', // the group of routes you want to protect
redirectRoute: '/login',
})
export const handle = sequence(/* ... */)
There also exists onlyPublicLoad
and createPublicRoutesHandle
functions for you to keep your authenticated users out of your login page (or any page you want).
If you're using the Firebase SDK on the client and have security rules for Firestore or Storage you will need to sync the server-side session with the client.
+layout.server.ts
file load the session and auth config object. This object is the Firebase SDK config object. If you're creating your own Firebase SDK client you don't need to load auth config here// +layout.server.ts
import { verifySession } from 'sveltekit-fireauth/server'
export const load = (event) => ({
authConfig: event.locals.auth.config,
session: verifySession(event),
})
+layout.ts
file get the session that was loaded from the server and create the Firebase Auth client using the auth config. If you're creating your own Firebase SDK client you don't need to create the Firebase Auth client here.// +layout.ts
import { createFirebaseAuth } from 'sveltekit-fireauth/client'
export const load = ({ data }) => ({
auth: createFirebaseAuth(data.authConfig),
session: data.session,
})
+layout.svelte
file use the syncAuthState
function inside an onMount callback.<!-- +layout.svelte -->
<script lang="ts">
import { onMount } from 'svelte'
import { syncAuthState } from 'sveltekit-fireauth/client'
export let data
$: ({ session, auth } = data)
onMount(() => {
const unsubscribe = syncAuthState(auth, session)
return () => {
unsubscribe()
}
})
</script>
It's possible to use OAuth with this library, however it requires a bit more setup, specially if you're using form actions. You may want to use an API route instead.
Assuming you're using an API route, here's what you need:
loginWithIdToken
helper to log in the user.// /login/oauth/+server.ts
import { error, json } from '@sveltejs/kit'
import { loginWithIdToken } from 'sveltekit-fireauth/server'
export const POST = async (event) => {
const { token } = await event.request.json()
if (!token) {
throw error(400, { message: 'Missing token' })
}
try {
await loginWithIdToken({ event, token })
} catch (e) {
throw error(401, { message: 'Unauthorized' })
}
return json({ success: true })
}
<script lang="ts">
import { invalidateAll } from '$app/navigation'
import { GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'
export let data
const googleLogin = async () => {
const provider = new GoogleAuthProvider()
const { user } = await signInWithPopup(data.auth, provider)
const token = user.getIdToken()
const response = await fetch('/login/oauth', {
method: 'POST',
body: JSON.stringify({ token }),
})
if (!response.ok) {
// handle login failure
await signOut(data.auth)
}
await invalidateAll()
}
</script>
<button on:click={googleLogin}>Sign in with Google</button>