js-sdk
js-sdk copied to clipboard
SSR support
I am currently exploring Pocketbase using their Pocketbase Js with Remix Js. The problem is that I can't adapt the operation of AuthStore with each request to the server.
Suggestion: To solve the drawback can an additional parameter to set the token manually.
pocketbaseClient.Users.getOne('idUser', {}, 'User eyJh...')
[Original] Compatibilidad con SSR
Actualmente estoy explorando Pocketbase usando su Pocketbase Js con Remix Js. El problema es que no puedo adaptar el funcionamiento de AuthStore con cada petición al servidor.
Sugerencia: Para solucionar el inconveniente pueden un parámetro adicional para poner el token manualmente
@thefersh Could you elaborate more a little on your use case and why the default AuthStore doesn't work for you?
You can create your own AuthStore and attach it during the client initialization. The only requirements is to be compatible with the AuthStore type interface. For example:
class MyCustomAuthStore {
token = "";
model = {};
isValid = false;
save(token, model) {
// implement save logic...
}
clear() {
// implement clear logic...
}
}
const client = new PocketBase("http://localhost:8090", 'en-US', new MyCustomAuthStore());
@ganigeorgiev you are right. can you make an example of implementation with Next Js or Remix Js? To be clear how I can implement AuthStore
The above example should be framework agnostic. There shouldn't be a need for a special nextjs/remix implementation.
Could you elaborate why the default implementation doesn't work in your case, so that I can understand better what actually prevents you to use it in nextjs/remix?
Hola, He logrado implementar autentificación para SSR, esto debería ser agnóstico de librerías o frameworks como nextjs/remix. en mi caso he utilizado sveltekit.
import PocketBase, { User, Admin } from 'pocketbase'
import { setCookie, parseCookies, destroyCookie } from 'nookies'
import jwt_decode from 'jwt-decode'
class CustomAuthStore {
private storageKey: string
constructor(storageKey = 'pocketbase') {
this.storageKey = storageKey
}
get isValid(): boolean {
if (!this.token) return false
const { exp } = jwt_decode<{ exp: number, id: string, type: string }>(this.token)
if (exp > Date.now() / 100) return false
return true
}
get token(): string {
const { token } = this.getCookie()
if (!token) return ''
return token
}
get model(): User | Admin | {} {
const { model } = this.getCookie()
if (!model) return {}
// admins don't have `verified` prop
if (typeof model?.verified !== 'undefined') {
return new User(model)
}
return new Admin(model)
}
private getCookie() {
const cookies = parseCookies()
const authorization = cookies[this.storageKey]
if (!authorization) return {}
return JSON.parse(authorization)
}
save(token: string, model: {}) {
setCookie(null, this.storageKey, JSON.stringify({ token, model }), {
maxAge: 30 * 24 * 60 * 60,
path: '/',
})
}
clear() {
destroyCookie(null, this.storageKey)
}
}
export const pocketbase = new PocketBase('http://localhost:8090', 'en-US', new CustomAuthStore())
If you are going with storing the auth token in a cookie, then I would recommend setting Secure
, HttpOnly
and SameSite=Strict
attributes (for more info - https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#security).
@oswaldohuillca in your AuthStore
you are not actually setting server side cookies right?
based on https://github.com/maticzav/nookies#reference, calling parseCookies
, setCookie
or destroyCookie
with ctx
to null
or undefined
will set client side cookies.
this means that it's not possible to set the HttpOnly
attribute as suggested by @ganigeorgiev, this is even reflected in the source code:
https://github.com/maticzav/nookies/blob/master/packages/nookies/src/index.ts#L111
if (isBrowser()) {
if (options && options.httpOnly) {
throw new Error('Can not set a httpOnly cookie in the browser.')
}
document.cookie = cookie.serialize(name, value, options)
so I guess we are back to square one.
I struggle to understand how one should get/set server side cookies within the AuthStore
interface. to do that, one would need to get/set the headers of response object, which is not passed as a parameter to save()
🤔
@ollema There are several ways to pass the the ctx/response object but the issue is that this object could have completely different API depending on the node framework you are using and I'm not sure if there is a general solution to this (I haven't had a chance to research it yet).
The response object could be provided during initialization as constructor argument to your custom store, as a setter/provider function (eg. withContext(ctx)
), etc.:
import PocketBase, { BaseAuthStore } from 'pocketbase';
class CustomAuthStore extends BaseAuthStore {
constructor(resp) {
super();
this.resp = resp;
}
...
save(token, model) {
super.save(token, model);
this.resp.setHeader(...)
}
clear() {
super.clear();
this.resp.setHeader(...)
}
}
const client = new PocketBase('http://127.0.0.1:8090', 'en-US', CustomAuthStore(resp));
The response object could be provided during initialization as constructor argument to your custom store, as a setter/provider function (eg.
withContext(ctx)
), etc.:... const client = new PocketBase('http://127.0.0.1:8090', 'en-US', CustomAuthStore(resp));
hmm, is the idea that you would create a new CustomAuthStore
and new client
for every request/response?
rather than keeping a "singleton" client export from a module? (I'm not sure if I'm using the correct terms here)
Yes, creating a new client instance for each request/response and using the cookie AuthStore to load the auth data is the easiest way to solve this, but as mentioned there are other approaches to load the response object depending on your use case.
Using a single global client/authStore instance may not always work since it could conflict with how node processes requests in the event loop (eg. startRequest1 -> wait for some db1/network1 call -> in the meantime startRequest2 -> wait for some db2/network2 call -> return db1 response -> return db2 response, etc.).
I'll try to spend some time on this researching the possible approaches, but that will be after v0.5.0 release of the main repo.
@ganigeorgiev figured I would give an update on the AuthStore
situation with SvelteKit SSR.
After doing some research it seems like this is the most "idiomatic" approach (for now, SvelteKit is still not 1.0 so things could change!):
Like you suggested you would use a new client instance for each request/response.
In SvelteKit, this could be handled in the handle
function in hooks.ts
.
src/hooks.ts
import type { Handle } from '@sveltejs/kit';
import PocketBase, { User } from 'pocketbase';
import cookie from 'cookie';
export const handle: Handle = async ({ event, resolve }) => {
const client = new PocketBase('http://127.0.0.1:8090');
event.locals.pocketbase = client;
const { token, user } = cookie.parse(event.request.headers.get('Cookie') ?? '');
if (!token || !user) {
return await resolve(event);
}
client.authStore.save(token, new User(JSON.parse(user)));
if (client.authStore.isValid) {
event.locals.user = client.authStore.model as User;
}
return await resolve(event);
};
By setting event.locals.pocketbase
to client
, the client will be available in handlers in +server.ts
and server-only load
functions (handle docs)
In the handle
function above, I have also populated event.locals.user
with the current user if the token is valid.
Now, while the handle
function above can be used to access the client and/or the current user in SSR, how do we authenticate a user or in other words store the token and (user)model?
If I understood it correctly, you would create server endpoints for this, for example:
src/routes/auth/signin/+server.ts
import type { RequestHandler } from '@sveltejs/kit';
import cookie from 'cookie';
const defaultCookieOptions = { maxAge: 30 * 24 * 60 * 60, path: '/', httpOnly: true, sameSite: true, secure: true };
export const POST: RequestHandler = async ({ request, locals }) => {
const response = new Response('{}');
const { email, password } = await request.json();
try {
const { token, user } = await locals.pocketbase.users.authViaEmail(email, password);
locals.pocketbase.authStore.save(token, user);
locals.user = user;
response.headers.append('Set-Cookie', cookie.serialize('token', token, defaultCookieOptions));
response.headers.append('Set-Cookie', cookie.serialize('user', JSON.stringify(user), defaultCookieOptions));
} catch {}
return response;
};
and
src/routes/auth/signout/+server.ts
import type { RequestHandler } from '@sveltejs/kit';
import cookie from 'cookie';
const defaultCookieOptions = { maxAge: -1, path: '/', httpOnly: true, sameSite: true, secure: true };
export const POST: RequestHandler = async ({ locals }) => {
const response = new Response('{}');
locals.pocketbase.authStore.clear();
locals.user = undefined;
response.headers.append('Set-Cookie', cookie.serialize('token', '', defaultCookieOptions));
response.headers.append('Set-Cookie', cookie.serialize('user', '', defaultCookieOptions));
return response;
};
It should be noted that the snippets above do not have any error handling, they can just be seen as inspiration.
To update the profile of the currently logged in user, you could add an additional /auth/save
route for example.
Final thoughts
- For PocketBase + SvelteKit SSR, the default
AuthStore
can be used today with the help ofhooks.ts
and some server endpoints - With that said, any official SSR
AuthStore
that could simplify the method above would be very nice! - If no official
AuthStore
for SSR will be added - then maybe (a revised) version of this method (and similar versions for other frameworks) could be added to the docs? - I am not experienced with either JS, Svelte, SvelteKit or PocketBase, so take this with a grain of salt 😅
@ollema I like your approach with the locals
and how you share the client instance from the hook context.
I would probably only move all cookie handling logic inside the hook handler so that all other server actions could operate only with the locals.client.authStore.save/clear
methods. Or in other words, your hook could have the following structure:
export const handle = async ({ event, resolve }) => {
const client = new PocketBase('http://127.0.0.1:8090');
event.locals.pocketbase = client;
// load auth data into the client from a request cookie
// ...
const response = await resolve(event);
// update the response cookie header(s) with the latest `client.authStore` state
// in case it was modified by some of the actions
// ...
return response;
};
For this week, I have planned after work to explore the SSR handling in the other frameworks (nextjs, nuxt2, nuxt3, remix), but I have doubts that there is a single solution that will work out of the box for all of them (even only for SvelteKit, depending which server handler you use and how you structure your app, there are different exported objects to access and update the request/response headers).
Supabase seems to deal with this by providing various auth helpers but I want to avoid that because it will be hard to maintain by myself in the long run.
I still have to do a more throughout research, but in general I think we can improve at least a little the SSR handling in 2 ways:
-
add 2 new cookie helper methods to the default auth store:
-
fromCookie(cookie: string, name = 'pb_auth')
- load auth data from the serialized cookie string -
toCookie(options, [existingCookieStrToAppendTo]): string
- export as serialized cookie header string
This way it will be up to the developers to decide how to load and set the cookie. We only will handle parsing and serialization.
-
-
create a SSR help document with some short examples and guides similar to yours for other frameworks (nextjs, nuxt2/3, remix)
As a side note, in order to support mixed browser and server-side usage at the same time, we also probably would have to set by default the HttpOnly
to false
(at the end it isn't too much different than using the LocalStorage).
My main cookie security related concern is with CSRF attacks, but that is handled by the SameSite=Strict
attribute.
For XSS prevention in general I'll add a note for the developers to consider defining a basic Content-Security-Policy (either set as meta tag or as HTTP header).
I've explored the available options the last couple of days, but I couldn't find a "one size fit all" solution, so I've implemented the following in the latest v0.6.0 SDK release:
-
I've added 2 cookie helper methods to the
BaseAuthStore
to simplify working with cookies:// update the store with the parsed data from the cookie string client.authStore.loadFromCookie('pb_auth=...'); // exports the store data as cookie, with option to extend the default SameSite, Secure, HttpOnly, Path and Expires attributes client.authStore.exportToCookie({ httpOnly: false }); // Output: 'pb_auth=...'
The exported cookie uses the token expiration date and also truncate the user model to its minimum (id, email, [verified]) in case it exceed 4096 bytes (this should be a very rare case).
-
I've added some examples for SvelteKit (based on @ollema suggestion), Nuxt 3 and Next.js in https://github.com/pocketbase/js-sdk#ssr-integration.
The above should help (or at least to give you some idea) how to deal with SSR, but if someone still have difficulties making it work, feel free to let me know and I'll try to provide some guidance based on your use case.
The two helper methods have greatly simplified auth for me (I am using SvelteKit).
Assuming the naming of the cookie is transparent to the user, isn't there a need for an orthogonal helper method to clear the auth cookie?
@aphilas As mentioned in the above discussion, the 2 helper methods only parse and serialize cookie strings and doesn't handle the actual cookie fetch/set because each framework uses different interface and methods for working with the server request and response objects (eg. it could be response.setHeader()
, or response.headers.set()
, or whatever else the framework is using; there are differences even within a single framework depending for example whether the underlying node server is express, h3 or else).
isn't there a need for an orthogonal helper method to clear the auth cookie?
No because if you are following the example from https://github.com/pocketbase/js-sdk#ssr-integration, the cookie will be set as "expired" on client.authStore.clear()
so you shouldn't have to bother manually deleting it. The end goal is to use only client.authStore
and leave the hook handle to take care for the cookie behind the scene.
Oh thanks, that makes sense. I was manually clearing the cookie.
@aphilas Also note that because by default client.authStore.exportToCookie()
will generate a cookie string with the HttpOnly
attribute, the cookie can be accessed and set only server-side (which is recommended), so you have to create a "logout" SvelteKit server action that calls client.authStore.clear()
, eg:
// src/routes/logout/+server.js
//
// creates a `POST /logout` server-side endpoint
export function POST({ request, locals }) {
locals.pocketbase.authStore.clear();
// return a message or you may want to redirect to some other page
return new Response('Success logout...');
}
If you want to be able to clear the cookie from both server-side and client-side, you'll have to:
- set explicitly HttpOnly to false, eg.
client.authStore.exportToCookie({ httpOnly: false })
- add a
onChange
listener to the browser client instance to set the cookie usingdocument.cookie
(or the experimentalCookieStore
which us currently supported only by Chrome/Edge), eg. something like:
client.authStore.onChange(() => {
document.cookie = client.authStore.exportToCookie({ httpOnly: false });
});
Thanks for the clarification.
I went for the first approach—the POST endpoint (so now they're called actions huh : D).
On Fri, Aug 26, 2022 at 9:24 PM Gani Georgiev @.***> wrote:
@aphilas https://github.com/aphilas Also note that because by default client.authStore.exportToCookie() will generate a cookie string with the HttpOnly attribute, the cookie can be accessed and set only server-side (which is recommended), so you have to create a "logout" SvelteKit server action that calls client.authStore.clear(), eg:
// src/routes/logout/+server.js//// creates a
POST /logout
server-side endpointexport function POST({ request, locals }) { locals.pocketbase.authStore.clear();// return a message or you may want to redirect to some other page return new Response('Success logout...');}
If you want to access the cookie from both server-side and client-side, you'll have to:
- set explicitly HttpOnly to false, eg. client.authStore.exportToCookie({ httpOnly: false })
- add a onChange listener to the client-side client to set the cookie using document.cookie (or the experimental CookieStore https://developer.mozilla.org/en-US/docs/Web/API/CookieStore which us currently supported only by Chrome/Edge), eg. something like:
client.authStore.onChange(() => { document.cookie = client.authStore.exportToCookie({ httpOnly: false });});
— Reply to this email directly, view it on GitHub https://github.com/pocketbase/js-sdk/issues/15#issuecomment-1228793115, or unsubscribe https://github.com/notifications/unsubscribe-auth/AHPTR2RSW3VQGWGAFZDMP7LV3EDXNANCNFSM54D5LGZA . You are receiving this because you were mentioned.Message ID: @.***>
I've explored the available options the last couple of days, but I couldn't find a "one size fit all" solution, so I've implemented the following in the latest v0.6.0 SDK release:
* I've added 2 cookie helper methods to the `BaseAuthStore` to simplify working with cookies: ```js // update the store with the parsed data from the cookie string client.authStore.loadFromCookie('pb_auth=...'); // exports the store data as cookie, with option to extend the default SameSite, Secure, HttpOnly, Path and Expires attributes client.authStore.exportToCookie({ httpOnly: false }); // Output: 'pb_auth=...' ``` > The exported cookie uses the token expiration date and also truncate the user model to its minimum (id, email, [verified]) in case it exceed 4096 bytes (this should be a very rare case). * I've added some examples for SvelteKit (based on @ollema suggestion), Nuxt 3 and Next.js in https://github.com/pocketbase/js-sdk#ssr-integration.
The above should help (or at least to give you some idea) how to deal with SSR, but if someone still have difficulties making it work, feel free to let me know and I'll try to provide some guidance based on your use case.
Hi I'm trying to follow the example for sveltekit but crashes on the hooks file the moment it calls the exportToCookie function, it throws:
Blob is not defined
ReferenceError: Blob is not defined
at n.t.exportToCookie (file:///C:/Users/Uribe/devs/svelte/gekkos-app/node_modules/pocketbase/dist/pocketbase.es.mjs:1:8183)
at Object.handle (/C:\Users\Uribe\devs\svelte\gekkos-app\src\hooks:18:74)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async respond (file:///C:/Users/Uribe/devs/svelte/gekkos-app/node_modules/@sveltejs/kit/src/runtime/server/index.js:215:20)
at async file:///C:/Users/Uribe/devs/svelte/gekkos-app/node_modules/@sveltejs/kit/src/vite/dev/index.js:383:22
my hook file:
import PocketBase from 'pocketbase';
export async function handle({ event, resolve }) {
event.locals.pocketbase = new PocketBase("http://127.0.0.1:8090", "es-ES");
// load the store data from the request cookie string
event.locals.pocketbase.authStore.loadFromCookie(event.request.headers.get('cookie') || '');
const response = await resolve(event);
// send back the default 'pb_auth' cookie to the client with the latest store state
response.headers.set('set-cookie', event.locals.pocketbase.authStore.exportToCookie({ httpOnly: false }));
return response;
}
I'm still learning so what could I be missing? 🤔
Thx in advance!
@IHummer Could you please check what version of node.js are you using? Blob
should be available in node 15.7+ (I'll consider making the Blob check option in the next release).
@IHummer I've just checked. Blob
is available in the global namespace in node 18, but in earlier version it is required to be imported from the buffer
package...
I'll publish shortly a v0.6.2 release making it optional since it is used only for a very edge case (when the cookie exceed 4096 and there are multi-byte characters like emojis in the serialized json).
@IHummer I've just checked.
Blob
is available in the global namespace in node 18, but in earlier version it is required to be imported from thebuffer
package...I'll publish shortly a v0.6.2 release making it optional since it is used only for a very edge case (when the cookie exceed 4096 and there are multi-byte characters like emojis in the serialized json).
I was using node v16.16 xd
Thanks for the support!
@IHummer I've just released a v0.6.2 of the SDK with a fallback check when Blob
is not available.
Please update your dependencies (npm update
) and try again.
@IHummer I've just released a v0.6.2 of the SDK with a fallback check when
Blob
is not available.Please update your dependencies (
npm update
) and try again.
I've just checked and it's working without problems
thanks!!
@ganigeorgiev It would probably be good to add a Nuxt2 example since it is the current stable version and has many more users than Nuxt3.
Confused about next js 13 implementation. Please Help
Started playing with solid-start and pocketbase. Couldn't make pocketbase work inside routeData function.
pb.authStore.model
@subhasish-smiles Please open a new issue with more details about your setup and steps to reproduce (pseudo-code/code sample would be also helpful).