Closed ticianomorvan closed 8 months ago
This seems like a common misunderstanding but I'm not sure how to explain it better.
First you need to clarify how do you plan to interact with the SDK:
Also, if it has anything to do with this, the token stored in localStorage it's not equal to the one served on the cookie.
I don't understand this. When the SDK is instantieted for example in getServerSideProps you don't have access anymore to the LocalStorage. Do you mean that you use the SDK both in the client and server? If that's the case then you'll have to change the CustomAuthStore
save
and clear
methods to get and set the cookie with some condition either from ServerRequest/ServerResponse
or from document.cookie
. In addition, when the mixed approach is used you have to export the cookie with httpOnly: false
in order to allow the js running in the browser to have access to it, aka: this.exportToCookie({ httpOnly: false })
Doing a little bit of debug and research, I can come to the conclusion that the cookie contained by the request may be not what the authStore expects, because, according to the examples, the key pb_auth= is set as the default, while my cookie has the following structure:
a_session_[code]_legacy=[token]
I'm not sure what this cookie is for. Is this some nextjs cookie or some other custom cookie?
In any case, you need to specify where your code is running (server, client or both). The CustomAuthStore
from above will work only in a server-side handler context. If you are not sure, you can add a console.log("foo")
when initializing the SDK store and see where the text is printed - in the browser console (aka. client) and/or in command line terminal (server).
First of all, I apologize if I wasn't clear enough. I was trying to run Pocketbase queries server-side for better performance, but it didn't work. The a_session...
cookie is the only one that appears to be generated from the OAuth2 flow, as well as it's created a localStorage entry with the user's model and token. But, as you said, localStorage doesn't have anything to do when we're on the server side.
The thing is that I can't understand how does the cookie that loadFromCookie
needs has to look like.
The thing is that I can't understand how does the cookie that loadFromCookie needs has to look like.
The default cookie entry should look something like this:
pb_auth=...encoded token and model json data...; Path=/; Expires=Thu, 27 Jun 2030 10:00:00 GMT; HttpOnly; Secure; SameSite=Strict
(the expires date should be the the same as the token "exp" claim)
The a_session... cookie is the only one that appears to be generated from the OAuth2 flow, as well as it's created a localStorage entry with the user's model and token.
If you have a localStorage entry, this means that you are currently handling the OAuth2 redirect in the browser. Or in other words, the PocketBase instance is running client-side. If you want to have a mixed SDK access (aka. making requests both client-side and server-side) then you'll have to export the cookie with exportToCookie({ httpOnly: false })
and change your store to conditionally get and set the cookie either from the ServerRequest/ServerResponse
objects or from document.cookie
depending on what environment it is running. Alternatively the browser/client SDK instance could use the authStore.onChange
listener instead of a custom store:
import PocketBase from 'pocketbase';
const client = new PocketBase("http://127.0.0.1:8090");
client.authStore.loadFromCookie(document.cookie);
client.authStore.onChange(() => {
document.cookie = client.authStore.exportToCookie({ httpOnly: false });
})
export default client;
(the above will always update the document.cookie that will be added automatically to every request from the browser to the node-server)
Could you provide a code sample/repo of what you are trying to do?
Well, I added the code you wrote and simply worked as I was expecting, being able to work client-side as well as server-side.
My final code looks something like this:
// lib/server.ts
import Pocketbase from 'pocketbase'
const client = new Pocketbase(process.env.NEXT_PUBLIC_POCKETBASE)
typeof document !== 'undefined' && client.authStore.loadFromCookie(document.cookie)
client.authStore.onChange(() => {
document.cookie = client.authStore.exportToCookie({ httpOnly: false })
})
export default client
A final question on this because I think it's solved, it's better to have an unique PocketBase instance or to instantiate it at every request?
I think there is some misunderstanding. The above will work only in a browser context because document.cookie
is not available in node. If you are making the requests in the browser while only using node for server rendering then that's OK.
But if you want to make requests from the node-server you'll need to read and set the cookie from the ServerRequest
and ServerResponse
objects (or their equivalent in the new nextjs if you are using nextjs13). When using PocketBase in a server context, you need a unique PocketBase instance on each server request because node is single threaded and requests are usually executed in an event loop, meaning that while you are waiting something to execute, the same process could process another client request and if you use only a single instance the data from the new request may overwrite the state from the initial one. In the browser this is not an issue and you can have a single instance for the entire lifecycle of the application.
I understand that modern frameworks blur the line between client and server but please make sure that your code is executed where you expect it to avoid accidentally leaking sensitive information. I still haven't got the time to explore the new api of nextjs13 and sometime after the v0.8.0 release I'll try to test it and will add a SSR example for it in the readme.
I need your NextJS 13 SSR example so much! I'm using NextJS 13 with PocketBase. I'm very confused with how to use PocketBase for user authentication in client components and then fetching user's data in server components.
In my project, I didn't get and set cookies (I don't know how to do that), and I'm not able to get user's data after user login. It is as if the PocketBase client in the server component that performs data fetching is never aware of the PocketBase client in the client component that performs user authentication.
Note: according to https://beta.nextjs.org/docs/api-reference/cookies, "the cookies
function is read-only. Outgoing requests cookies cannot be set
or delete
". It also says that "The Next.js team at Vercel is working on adding the ability to set cookies in addition to the cookies
function.".
I'm not sure if this implies that currently we are not able to set cookies when making requests from the server.
I need your NextJS 13 SSR example so much! I'm using NextJS 13 with PocketBase. I'm very confused with how to use PocketBase for user authentication in client components and then fetching user's data in server components.
In my project, I didn't get and set cookies (I don't know how to do that), and I'm not able to get user's data after user login. It is as if the PocketBase client in the server component that performs data fetching is never aware of the PocketBase client in the client component that performs user authentication.
In my case, I was working with Next.js 12 (mostly it was something I knew better) so I can't give you a precise example. What I can say to you is that, while I was working on it, Next didn't really append any cookie to the requests, I read someone said that it was because of the localhost but I didn't investigate further on that. After Gani's reply, I was able to work with the cookie as it was being exported to the server side when it changed. So maybe you can try to append that onChange method to your Pocketbase's instance and then using the exported model for your queries, but I really don't know how Pocketbase works in Next.js 13
In my case, I was working with Next.js 12 (mostly it was something I knew better) so I can't give you a precise example. What I can say to you is that, while I was working on it, Next didn't really append any cookie to the requests, I read someone said that it was because of the localhost but I didn't investigate further on that. After Gani's reply, I was able to work with the cookie as it was being exported to the server side when it changed. So maybe you can try to append that onChange method to your Pocketbase's instance and then using the exported model for your queries, but I really don't know how Pocketbase works in Next.js 13
Thank you for your reply! I tried a little bit and it seems to solve my current problem, which is to get data on the server side after user login. Following Gani's suggestion, I create a new PocketBase client instance, get the cookie using the cookies().get('pb_auth')
method in Next.js 13, which returns an object {name: 'pb_auth', value: '{...}'}
, and then convert it to a string, then load the cookie string with authStore.loadFromCookie
. Now I'm able to read data using client.records.getList
. A minimal example:
// app/something/page.tsx. This is a server component.
import PocketBase from 'pocketbase';
import { cookies } from 'next/headers';
export default async function Home() {
const client = new PocketBase('http://127.0.0.1:8090');
const nextCookies = cookies();
const cookie_object = nextCookies.get('pb_auth');
const cookie_string = cookie_object.name + '=' + cookie_object.value;
client.authStore.loadFromCookie(cookie_string);
const data = await client.records.getList('collection_name', 1, 100);
return (
<div>
<h1>Results</h1>
<div>
{data.items.map(item => {
return <SomeComponent key={item.id} item={item}/>;
})}
</div>
</div>
)
}
I'm quite new to web development and not very familiar with JS/TS, so maybe there are better ways to write the code.
Hey! I didn't know about cookies.get()
method, seems to be the way to follow in this case. Don't apologize for being new to webdev, everyone starts from somewhere! Also, If you get to a stable solution, try to share it with other people! It's all about cooperative work. Good work :)
So what I did for my solution is the following...
import Cookies from "cookies";
import { GetServerSideProps } from "next";
import client from "@services/config";
interface Data {
firstName: string;
lastName: string;
id: string;
avatar: string;
isLoggedIn: boolean;
}
export const getServerSideProps: GetServerSideProps<{
userData: Data;
}> = async (context) => {
const { req, res } = context;
const cookies = new Cookies(req, res);
const pbCookie = cookies.get("pb_auth");
if (!pbCookie) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
client.authStore.loadFromCookie(`pb_auth=${pbCookie}`);
const data = await getUserProfile();
const userData = {
firstName: data.record.firstName,
lastName: data.record.lastName,
id: data.record.id,
avatar: data.record.avatar,
isLoggedIn: true,
};
return {
props: {
userData,
},
};
};
So the above helps me on the server and then for the client I have the following.
import PocketBase from "pocketbase";
export const client = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_BASE_URL
);
typeof document !== "undefined" &&
client.authStore.loadFromCookie(document.cookie);
client.authStore.onChange(() => {
if (typeof document !== "undefined") {
document.cookie = client.authStore.exportToCookie({ httpOnly: false });
}
});
export default client;
I've managed to get it working in the new app
directory, using (https://github.com/vvo/iron-session/issues/560#issuecomment-1324598048 as a base. The only thing to note is that any actions that would update the authStore
can only happen in the client, fow now.
lib/getUserFromCookie.ts
lib/pocketbase.ts
And then, in
app/layout.tsx
Also note that currently there's no way to send headers to the client inside a Server Component, it's WIP atm. (This means that refreshing the cookie everytime the store updates is not possible)
I'm guessing as long as we're not doing anything to the authStore
on the server, just keeping the same logic and leaving the set-cookie
part out will work fine. But I'm just wondering is there any issue's security wise or any-other wise?
Also would add the cookie to document.cookie
add the cookie to every request not just requests to the nextjs server but even to thirdparty api servers?
@samit43
I'm not sure what are your concern. If you are not using the SDK server side then the token will be stored in the browser's LocalStorage. For SSR you'll need a cookie flow if you want to "persist" the auth state between requests. It is safe as long as your cookie is set with Secure
and SameSite=Strict
attributes (which is the default when calling exportToCookie
).
Oh, thank you! Sorry, I missed the SameSite=Strict
and Secure
attributes
thx @rafifos Your solution works great. The nextjs 13 is a hard one to play with....in comparison SvelteKit is such a better expirence....
@rafifos solution is great, but what if the user data changed like the username, and on the user browser the cookie still the same with the old username, this means that the server and the browser will have incorrect user data with an old username. I think refreshing the authStore should happen on the server to get the updated user data
What's about using route handlers (https://nextjs.org/docs/app/building-your-application/routing/route-handlers) to get and set cookies?
My following code is not working, because onChange
gets never triggered, but maybe it's an inspiration.
// app/api/pocketbase/route.ts
import { NextRequest, NextResponse } from "next/server";
import PocketBase from "pocketbase";
export async function GET(request: NextRequest) {
const pb = new PocketBase("https://my-pock.pockethost.io");
let authCookie = "";
if (request.cookies.has("pb_auth")) {
const cookie = request.cookies.get("pb_auth");
authCookie = `${cookie?.name}=${cookie?.value}`;
}
pb.authStore.loadFromCookie(authCookie);
pb.authStore.onChange(() => {
const [key, value] = pb.authStore.exportToCookie().split("=");
request.cookies.set(key, decodeURIComponent(value));
});
try {
pb.authStore.isValid && (await pb.collection("users").authRefresh());
} catch (_) {
pb.authStore.clear();
}
return NextResponse.json({ pb_auth: pb.authStore.exportToCookie() });
}
// app/page
export default async function Home() {
const res = await fetch('http://localhost:3000/api/pocketbase', {
cache: 'no-cache',
});
const { pb_auth } = await res.json();
const pb = new PocketBase("https://my-pock.pockethost.io");
pb.authStore.loadFromCookie(decodeURIComponent(pb_auth));
This is how I use it with app router
I have;
fetchers.ts
handling cache and fetching logicactions.ts
handling actions ("use server") on both client side and as serverActionmiddleware.ts
to protect routes and handle auth page redirectionRemember to invalidateTag
for fetchers to get refreshed data on mutation
Planning to release a pocketbase version of my paid nextjs starter kit Next Edge Starter soon (can remove this line if it is disturbing the author)
I was able to simplify what I shared above after checking supabase's auth helpers implementation for nextjs. Basically separated server
and client
initializations and using cookie for checking authentication. Seems to work well for me but I never used pocketbase on production
{post.title}
})}@orenaksakal thank's a lot for sharing your code, i gonna try this in my application. One question, why do you rederict
, if pathName starts with /auth
?
why do you rederict, if pathName starts with /auth?
It is optional but i find it useful to redirect user to dashboard if user is already authenticated and trying to reach auth pages
@orenaksakal it seems, that initPocketBaseServer()
is not working correct. I see the following logs:
/api/collections/users/auth-refresh: 401
{
"errorDetails": "<nil>",
"errorMessage": "The request requires valid record authorization token to be set."
}
I think this happen on line await pocketbase.collection("users").authRefresh();
I read to docs again and can't find any reasons why this error occurs. Do you have any idea?
Do you have any idea?
No idea TBH, but it works for me on both local and vercel domain one thing I never tried is logging in as admin
@orenaksakal if I used initPocketBaseServer in multi rsc inside a page, is it not bad? because pocketbase.collection("users").authRefresh() will execute multi times
is it not bad?
I think it is the same case when you use sveltekit's handle server hook see it will be executed on each request but incase its something to be prevented one way to do it would be having 3 instances middleware, server, client where middleware handles auth refreshes only and other two is for environment specific work
Note: I have been looking at pocketbase only for couple of days its likely there is huge room for improvement on what I shared
response?.headers.set("set-cookie", pocketbase.authStore.exportToCookie());
I want to keep pocketbase completely on the server side but I cannot find a way to update the cookie from there, your solution with the response did not work for me.
response?.headers.set("set-cookie", pocketbase.authStore.exportToCookie());
I want to keep pocketbase completely on the server side but I cannot find a way to update the cookie from there, your solution with the response did not work for me.
I managed using the Next.js cookies()
:
"use server";
import PocketBase from "pocketbase";
import { cookies } from "next/headers";
import { RequestCookie } from "next/dist/compiled/@edge-runtime/cookies";
export interface PocketbaseLogin {
token?: string;
name?: string;
email?: string;
isAdmin?: boolean;
error?: { message: string };
}
const pb = new PocketBase(process.env.PB_URL);
const loginPb = async ({ email, password }: { email: string; password: string }) => {
let response: PocketbaseLogin = {};
try {
const authData = await pb.collection("users").authWithPassword(email, password);
if (authData) {
const record = authData.record;
console.log(`User ${record.name} authenticated`);
// set the client cookie
cookies().set("pb_auth", pb.authStore.exportToCookie());
response.token = authData.token;
response.name = record.name;
response.email = record.email;
response.isAdmin = record.isAdmin;
}
} catch (err: any) {
console.log(`${email} ${err.message}`);
response.error = err.response;
}
Here is what I have done to fix this for Next 13
Hey, I have written functions returning PocketBase instances for both Server and Client components. I have done it in a style @supabase/ssr
does it for ease of understanding
For client components : This instantiates a PocketBase instance using the singleton pattern, so you can call it anywhere you want. It also syncs the cookie with localStorage, thus seamlessly responding to auth state change
For server components : This instantiates a PocketBase instance for each call, you can pass it a cookieStore, in which case you will get the authStore instantiated. For static routes, you can use it without passing any cookieStore.
Middleware : You can have a middleware.ts
at the root of your project or the src
folder with matching paths for protected route. If user is not authenticated, redirect to your intended path.
Note :
NEXT_PUBLIC_POCKETBASE_API_URL
, you must have this in your .env
filepocketbase-typegen
by @patmood, you can generate the types using this package and place in @types/pocketbase-types.ts
or a befitting path and update the import paths.This is really amazing work @tsensei! For the middleware.ts, /((?!api|_next/static|_next/image|favicon.ico|login|phrase|$).*)
, changing ^$
-> $
excluded the home page for me
I am curious about PocketBase. After reading your solution, I do have a question. Please forgive me if this is a silly question as I am pretty new to web development. 🙏 In production server, our domains for PB would not be localhost. Probably as this scenario, NextJS is on -p 3000 and PB on -p 8090 Then we might bind -p 443 to NGINX as reverse proxy. So we need to split PB to a subdomain. NextJS App - https://mydoman.com PocketBase - https://pb.mydomain.com
In this case, I want to understand how PocketBase cookie from authStore.exportToCookie() sign the domain? If this export the cookie domain as “.mydomain.com”, since (.) can share to root domain, it should be accessible from my domain.com. If it export cookie domain as “api.mydomain.com”, how can I access the cookie from root domain. (This is my understanding and please correct me if it is incorrect)
How does it work actually with this solution ? I really wish to learn that part a little more so the I can secure route from server side. Marry Christmas to you!
@KweeBoss, as you need the cookie accessible to the root domain, you can just export it with the root domain and it should be accessible too all subdomains if I recall correctly.
Thank you so much @tsensei 🙏. I have found it. export interface SerializeOptions { encode?: (val: string | number | boolean) => string, maxAge?: number, domain?: string, path?: string, expires?: Date, httpOnly?: boolean, secure?: boolean, priority?: string, sameSite?: boolean|string, }
Hi I have issues with request not having Authorization header added to PB request from server. After logging in as admin and having set the cookie with the logged in token.
I have this setup for the pocketbase server client on my server actions
async function initPocketbase() {
const pb = new PocketBase("http://127.0.0.1:8090");
// load the store data from the request cookie string
const authCookie = cookies().get("pb_auth");
if(authCookie) {
pb.authStore.loadFromCookie(`${authCookie.name}=${authCookie.value}`);
console.log("Should be here", pb.authStore.token)
try {
// get an up-to-date auth store state by verifying and refreshing the loaded auth model (if any)
pb.authStore.isValid && (await pb.admins.authRefresh());
} catch (_) {
// clear the auth store on failed refresh
pb.authStore.clear();
redirect("/login");
}
}
// send back the default 'pb_auth' cookie to the client with the latest store state
pb.authStore.onChange((token, model) => {
console.log("valid", pb.authStore.isValid)
console.log("token", token);
cookies().set("pb_auth", pb.authStore.token, {
httpOnly: true
});
});
return pb;
}
export default initPocketbase;
Cookie is available and has this pb_auth=eyJhbGciOiJIUzI1N...
format when used in loadFromCookie
But it looks like loadFromCookie
never resolves to setting the token and updating the model in Auth Store.
I tried with both admin auth and user auth.
The components that uses the code above lookes like this
export async function getOrders() {
const pb = await initPocketbase()
const records = await pb.collection("orders").getFullList({
sort: "-created",
});
return records;
}
The orders endpoint for now only allows admins, but in the PB logs all request are done as quest.
@bentrynning Are you sure that authCookie.value
is a raw serialized cookie string and not the token or the json object that contains the token?
Note you don't have to use pb.authStore.loadFromCookie/exportToCookie
if you are using the Next.js cookies abstraction. To load a token directly into the store you can call pb.authStore.save(token)
.
In any case, I'm closing this issue as it is starting to become too off-topic and there isn't any clear actionable item here.
I've tried to check if anything has changed in Next.js couple months ago and unfortunately I wasn't able to find an easy solution for sharing the SDK AuthStore state between server components and I'm reaching a point where I'd rather not recommend using PocketBase with Next.js as it feels too much work for very little benefit, at least to me.
If someone has a suggestion without having to maintain special Next.js helpers, feel free to open a PR with an example for the README.
ganigeorgiev Thanks for the reply, and sorry for adding just another of topic question to this thread. Next only gives you the cookie name and cookie value trough the cookie api. So it is not a serialized cookie string. But just saw all the other examples and looked like it was working for them with just a cookieName=cookieValue (token) string.
I'll try to set the token directly in the store then! Thanks for the tip.
By the way thanks for the tremendous nice work you have done with PocketBase <3 very cool project!
I've followed the examples displayed in the README, as well as looked for solutions in other frameworks but I simply can't fix this problem.
Doing a little bit of debug and research, I can come to the conclusion that the cookie contained by the request may be not what the
authStore
expects, because, according to the examples, the keypb_auth=
is set as the default, while my cookie has the following structure:Also, if it has anything to do with this, the token stored in localStorage it's not equal to the one served on the cookie.
As extra information, I leave the code I wrote.
Versions: pocketbase v0.7.10; pocketbase (sdk) v0.7.4