Handle Firebase auth change in layout.server.ts #10189
Replies: 3 comments 1 reply
-
Redirects need to be thrown/rejected. If that doesn't work, I'd recommend using |
Beta Was this translation helpful? Give feedback.
-
I just solved this exact issue, I'm trying to protect certain routes so that the user is redirected to the login page if they aren't authenticated. What you will need to use is the Here is my // src/hooks.server.ts
import { auth } from '$lib/server/firebase';
import type { Handle } from '@sveltejs/kit';
import type { DecodedIdToken } from 'firebase-admin/auth';
function redirect(location: string, body?: string) {
return new Response(body, {
status: 303,
headers: { location },
});
}
async function decodeToken(token?: string): Promise<DecodedIdToken | null> {
if (!token || token === 'null' || token === 'undefined') return null;
try {
return await auth.verifyIdToken(token);
} catch (err) {
return null;
}
}
export const handle: Handle = async function ({ event, resolve }) {
// Retrieve and decode the user ID token
const token = event.cookies.get('token');
const decodedToken = await decodeToken(token);
if (decodedToken) {
// Add the decoded token as custom data to the request,
// which is passed to handlers in +server.js and server load functions
event.locals.user = decodedToken;
}
// Check if the request is trying to access restricted routes without authorization
if (!decodedToken && event.url.pathname.startsWith('/app')) {
return redirect('/auth/login');
}
// Load page as normal
const response = await resolve(event);
return response;
}; Note that you can't throw a redirect from the handle function, you'll need to return a Since we are both using Firebase, it may be helpful for me to mention how I am checking user auth on the server side. The server doesn't have access to the client auth, so we need to create a cookie that will be sent with every request to the server. Here is the code I'm using to setup Firebase services and automatically create the cookie that the server can use to validate user auth: // src/lib/client/firebase.ts
import { browser, dev } from '$app/environment';
import { user } from '$lib/stores/auth';
import cookie from 'cookie';
import { getApps, initializeApp, type FirebaseApp } from 'firebase/app';
import { connectAuthEmulator, getAuth, type Auth } from 'firebase/auth';
import { Firestore, connectFirestoreEmulator, doc, getFirestore, setDoc } from 'firebase/firestore';
import { connectStorageEmulator, getStorage, type FirebaseStorage } from 'firebase/storage';
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: '...',
authDomain: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...',
measurementId: '...',
};
export let app: FirebaseApp;
export let auth: Auth;
export let db: Firestore;
export let storage: FirebaseStorage;
// Initialize Firebase
if (!getApps().length) {
app = initializeApp(firebaseConfig);
auth = getAuth(app);
db = getFirestore(app);
storage = getStorage(app);
// Connect to Firebase emulators during local development
if (dev) {
connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true });
connectFirestoreEmulator(db, 'localhost', 8080);
connectStorageEmulator(storage, 'localhost', 9199);
}
// Only run in the browser, otherwise this will run during SSR and will break
if (browser) {
// Add an observer to create or update the 'token' cookie when the user's ID token changes
// This is triggered on sign-in, sign-out, and token refresh events
auth.onIdTokenChanged(async (newUser) => {
const token = await newUser?.getIdToken();
// Create a cookie for the token or delete it if token is undefined
document.cookie = cookie.serialize('token', token ?? '', {
path: '/',
maxAge: token ? undefined : 0,
});
// Update the global client user store with the new user
user.set(newUser);
});
// Refresh the ID token every 10 minutes
setInterval(async () => {
if (auth.currentUser) {
// Force a ID token refresh event, triggers observer above
await auth.currentUser.getIdToken(true);
}
}, 10 * 60 * 1000);
}
} Note: I am using the cookie package for managing cookies on the client. Here is my Firebase server code: // src/lib/server/firebase.ts
import { dev } from '$app/environment';
import { DocumentExistsError } from '$lib/utils/errors';
import { getApps, initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import { getFirestore } from 'firebase-admin/firestore';
import type { App } from 'firebase-admin/lib/app/core';
if (dev) {
// Connect the Admin SDK to Firebase emulators during local development
process.env['FIREBASE_AUTH_EMULATOR_HOST'] = 'localhost:9099';
process.env['FIRESTORE_EMULATOR_HOST'] = 'localhost:8080';
// Necessary to verify ID tokens with the Admin SDK
// See: https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_the_firebase_admin_sdk
process.env['GOOGLE_CLOUD_PROJECT'] = 'Your Firebase Project ID';
}
function makeApp(): App {
const apps = getApps();
if (apps.length > 0) {
return apps[0];
}
return initializeApp();
}
export const app = makeApp();
export const auth = getAuth(app); I created this code by referencing this article, this repo, Firebase Docs, and the official SvelteKit docs on hooks. |
Beta Was this translation helpful? Give feedback.
-
Wow that is awesome thank you so much for taking the time to reply. I ended
up looking at Next.js in the end as I seemed to be able to find more
information to progress. But I will certainly review this at some point in
the future!!
…On Wed, 26 Jul 2023, 19:52 Caidan Williams, ***@***.***> wrote:
I just solved this exact issue, I'm trying to protect certain routes so
that the user is redirected to the login page if they aren't authenticated.
What you will need to use is the src/hooks.server.ts file to handle
redirection. This is more secure because in a layout.svelte file it can
still load and return data to the client, since the client would be
handling navigation in that case.
Here is my hooks.server.ts file:
// src/hooks.server.ts
import { auth } from '$lib/server/firebase';import type { Handle } from ***@***.***/kit';import type { DecodedIdToken } from 'firebase-admin/auth';
function redirect(location: string, body?: string) {
return new Response(body, {
status: 303,
headers: { location },
});}
async function decodeToken(token?: string): Promise<DecodedIdToken | null> {
if (!token || token === 'null' || token === 'undefined') return null;
try {
return await auth.verifyIdToken(token);
} catch (err) {
return null;
}}
export const handle: Handle = async function ({ event, resolve }) {
// Retrieve and decode the user ID token
const token = event.cookies.get('token');
const decodedToken = await decodeToken(token);
if (decodedToken) {
// Add the decoded token as custom data to the request,
// which is passed to handlers in +server.js and server load functions
event.locals.user = decodedToken;
}
// Check if the request is trying to access restricted routes without authorization
if (!decodedToken && event.url.pathname.startsWith('/app')) {
return redirect('/auth/login');
}
// Load page as normal
const response = await resolve(event);
return response;};
Note that you can't throw a redirect from the handle function, you'll need
to return a 3XX response with a location header. I've abstracted this
into a function for easier use.
Since we are both using Firebase, it may be helpful for me to mention how
I am checking user auth on the server side. The server doesn't have access
to the client auth, so we need to create a cookie that will be sent with
every request to the server.
Here is the code I'm using to setup Firebase services and automatically
create the cookie that the server can use to validate user auth:
// src/lib/client/firebase.ts
import { browser, dev } from '$app/environment';import { user } from '$lib/stores/auth';import cookie from 'cookie';import { getApps, initializeApp, type FirebaseApp } from 'firebase/app';import { connectAuthEmulator, getAuth, type Auth } from 'firebase/auth';import { Firestore, connectFirestoreEmulator, doc, getFirestore, setDoc } from 'firebase/firestore';import { connectStorageEmulator, getStorage, type FirebaseStorage } from 'firebase/storage';
// Your web app's Firebase configuration// For Firebase JS SDK v7.20.0 and later, measurementId is optionalconst firebaseConfig = {
apiKey: '...',
authDomain: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...',
measurementId: '...',};
export let app: FirebaseApp;export let auth: Auth;export let db: Firestore;export let storage: FirebaseStorage;
// Initialize Firebaseif (!getApps().length) {
app = initializeApp(firebaseConfig);
auth = getAuth(app);
db = getFirestore(app);
storage = getStorage(app);
// Connect to Firebase emulators during local development
if (dev) {
connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true });
connectFirestoreEmulator(db, 'localhost', 8080);
connectStorageEmulator(storage, 'localhost', 9199);
}
// Only run in the browser, otherwise this will run during SSR and will break
if (browser) {
// Add an observer to create or update the 'token' cookie when the user's ID token changes
// This is triggered on sign-in, sign-out, and token refresh events
auth.onIdTokenChanged(async (newUser) => {
const token = await newUser?.getIdToken();
// Create a cookie for the token or delete it if token is undefined
document.cookie = cookie.serialize('token', token ?? '', {
path: '/',
maxAge: token ? undefined : 0,
});
// Update the global client user store with the new user
user.set(newUser);
});
// Refresh the ID token every 10 minutes
setInterval(async () => {
if (auth.currentUser) {
// Force a ID token refresh event, triggers observer above
await auth.currentUser.getIdToken(true);
}
}, 10 * 60 * 1000);
}}
Note: I am using the cookie <https://www.npmjs.com/package/cookie>
package for managing cookies on the client.
Here is my Firebase server code:
// src/lib/server/firebase.ts
import { dev } from '$app/environment';import { DocumentExistsError } from '$lib/utils/errors';import { getApps, initializeApp } from 'firebase-admin/app';import { getAuth } from 'firebase-admin/auth';import { getFirestore } from 'firebase-admin/firestore';import type { App } from 'firebase-admin/lib/app/core';
if (dev) {
// Connect the Admin SDK to Firebase emulators during local development
process.env['FIREBASE_AUTH_EMULATOR_HOST'] = 'localhost:9099';
process.env['FIRESTORE_EMULATOR_HOST'] = 'localhost:8080';
// Necessary to verify ID tokens with the Admin SDK
// See: https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_the_firebase_admin_sdk
process.env['GOOGLE_CLOUD_PROJECT'] = 'Your Firebase Project ID';}
function makeApp(): App {
const apps = getApps();
if (apps.length > 0) {
return apps[0];
}
return initializeApp();}
export const app = makeApp();export const auth = getAuth(app);
I created this code by referencing this article
<https://jeroenpelgrims.com/access-the-firebase-auth-user-in-sveltekit-server-side/>,
this repo
<https://github.com/ManuelDeLeon/sveltekit-firebase-ssr/blob/main/src/hooks.server.ts>,
Firebase Docs
<https://firebase.google.com/docs/auth/admin/verify-id-tokens#web>, and
the official SvelteKit docs on hooks.
—
Reply to this email directly, view it on GitHub
<#10189 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAXY5IE6RA2U5MT5EDAT4K3XSFRPHANCNFSM6AAAAAAZLVOU6A>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Hi,
I am trying to implement the onAuthStateChanged function into the load function inside a +layout.server.ts file to protect my authorised/protected pages. I can't seem to make it redirect to my login page if not logged in though. I wondered if anyone has achieved this? I have tried this in a layout.svelte file, using an onMount function but the page briefly flashes up before redirecting which I would like to get away from u less someone can convince me that it's ok to ended something like "Loading..." if there isn't a user and sensitive content if there is a user. I've always thought it should be handled serverside though, and I'm new to SvelteKit. This is my code, can anyone help?
Beta Was this translation helpful? Give feedback.
All reactions