Open dChunikhin opened 7 months ago
Hi Denis,
authentication using Next.js is unfortunately a bit more complex :/
As you noticed, your client components will be rendered on both the server and in the client - and of course, on the server you cannot access the browser's localStorage
.
What I would recommend is using cookies.
Best would be if your Django server would be accessible from the same domain, because then you can share cookies between your Django server and your Next.js server.
In that scenario, the Django server would set authentication cookies (httpOnly, secure).
Requests from your Browser to your Django server would automatically include those cookies, so no need to do any localStorage
stuff in your client.
Then you would need to get access to those cookies during SSR. Unfortunately, the SSR run of Next.js has no direct access to cookies - you would need to pass them into your Client Components as props from a RSC component that can access cookies. To ensure that these secrets don't leak into the browser, you could use the https://github.com/phryneas/ssr-only-secrets package.
for reference, this is how I did something similar, not sure if it is the best way:
"use client";
import { ApolloLink, HttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
NextSSRApolloClient,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { readSSROnlySecret } from "ssr-only-secrets";
const getAuthLink = (cloakedSessionId: Promise<string>) =>
setContext(async (_, { headers }) => {
const sessionId = await readSSROnlySecret(
// @ts-ignore
await cloakedSessionId,
"SECRET_KEY",
);
if (sessionId) {
return {
headers: {
...(headers || {}),
Cookie: `sessionid=${sessionId}`,
},
};
}
return { headers };
});
export function ApolloWrapper({
children,
sessionId,
}: React.PropsWithChildren<{ sessionId: Promise<string> }>) {
function makeClient() {
const httpLink = new HttpLink({
uri:
typeof window === "undefined"
? "http://localhost:8000/graphql"
: "/graphql",
fetchOptions: {
cache: "no-store",
crendentials: "include",
},
});
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
getAuthLink(sessionId),
httpLink,
])
: httpLink,
});
}
return (
<ApolloNextAppProvider makeClient={makeClient}>
{children}
</ApolloNextAppProvider>
);
}
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ApolloWrapper } from "@/components/apollo-wrapper";
import { cookies } from "next/headers";
import { cloakSSROnlySecret } from "ssr-only-secrets";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const sessionIdPlain = cookies().get("sessionid")?.value;
const sessionId = cloakSSROnlySecret(sessionIdPlain ?? "", "SECRET_KEY");
return (
<ApolloWrapper sessionId={sessionId}>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ApolloWrapper>
);
}
I am not getting authorization in headers in the apollo server context!
The Error in next.js console My ENV.. Note: I have not generated any envs like
In layout.tsx
const token = cookies().get("next-auth.session-token")?.value;
const sessionId = cloakSSROnlySecret(token ?? "", "SECRET_KEY");
<ApolloWrapper sessionId={sessionId}> ...
in apollo-wrapper.tsx
"use client";
import { ApolloLink, HttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import {
ApolloNextAppProvider,
NextSSRApolloClient,
NextSSRInMemoryCache,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { readSSROnlySecret } from "ssr-only-secrets";
const getAuthLink = (cloakedSessionId: Promise<string>) =>
setContext(async (_, { headers }) => {
const sessionId = await readSSROnlySecret(
// @ts-ignore
await cloakedSessionId,
"SECRET_KEY"
);
if (sessionId) {
return {
headers: {
...headers,
authorization: sessionId ? `Bearer ${sessionId}` : "",
},
};
}
return { headers };
});
export function ApolloWrapper({
children,
sessionId,
}: React.PropsWithChildren<{ sessionId: Promise<string> }>) {
function makeClient() {
const httpLink = new HttpLink({
uri: typeof window === "undefined" ? "http://localhost:8000/graphql" : "http://localhost:8000/graphql",
fetchOptions: {
cache: "no-store",
crendentials: "include",
},
});
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
getAuthLink(sessionId),
httpLink,
])
: httpLink,
});
}
return <ApolloNextAppProvider makeClient={makeClient}>{children}</ApolloNextAppProvider>;
}
@sayandedotcom if you log it, do you have the cookies available? Are they sent from the browser if you look into the network tab?
yes , I logged it and it was available and it is working fine with the server component
Server Component
import { cookies } from "next/headers";
import { ApolloClient, InMemoryCache, createHttpLink, from } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { onError } from "@apollo/client/link/error";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
const httpLink = createHttpLink({
uri: "http://localhost:8000/graphql",
credentials: "include",
fetchOptions: { cache: "no-store" },
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log(graphQLErrors);
}
if (networkError) {
// handle network error
console.log(networkError);
}
});
const authLink = setContext((_, { headers }) => {
const cookieStore = cookies();
const token =
cookieStore.get("__Secure-next-auth.session-token") ?? cookieStore.get("next-auth.session-token");
return {
headers: {
...headers,
authorization: token ? `Bearer ${token.value}` : "",
},
};
});
const appLink = from([errorLink, httpLink]);
export const { getClient } = registerApolloClient(() => {
return new ApolloClient({
link: authLink.concat(appLink),
cache: new InMemoryCache(),
defaultOptions: {
query: {
errorPolicy: "all",
},
mutate: {
errorPolicy: "all",
},
},
});
});
for reference, this is how I did something similar, not sure if it is the best way:
"use client"; import { ApolloLink, HttpLink } from "@apollo/client"; import { setContext } from "@apollo/client/link/context"; import { ApolloNextAppProvider, NextSSRInMemoryCache, NextSSRApolloClient, SSRMultipartLink, } from "@apollo/experimental-nextjs-app-support/ssr"; import { readSSROnlySecret } from "ssr-only-secrets"; const getAuthLink = (cloakedSessionId: Promise<string>) => setContext(async (_, { headers }) => { const sessionId = await readSSROnlySecret( // @ts-ignore await cloakedSessionId, "SECRET_KEY", ); if (sessionId) { return { headers: { ...(headers || {}), Cookie: `sessionid=${sessionId}`, }, }; } return { headers }; }); export function ApolloWrapper({ children, sessionId, }: React.PropsWithChildren<{ sessionId: Promise<string> }>) { function makeClient() { const httpLink = new HttpLink({ uri: typeof window === "undefined" ? "http://localhost:8000/graphql" : "/graphql", fetchOptions: { cache: "no-store", crendentials: "include", }, }); return new NextSSRApolloClient({ cache: new NextSSRInMemoryCache(), link: typeof window === "undefined" ? ApolloLink.from([ new SSRMultipartLink({ stripDefer: true, }), getAuthLink(sessionId), httpLink, ]) : httpLink, }); } return ( <ApolloNextAppProvider makeClient={makeClient}> {children} </ApolloNextAppProvider> ); }
import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; import { ApolloWrapper } from "@/components/apollo-wrapper"; import { cookies } from "next/headers"; import { cloakSSROnlySecret } from "ssr-only-secrets"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const sessionIdPlain = cookies().get("sessionid")?.value; const sessionId = cloakSSROnlySecret(sessionIdPlain ?? "", "SECRET_KEY"); return ( <ApolloWrapper sessionId={sessionId}> <html lang="en"> <body className={inter.className}>{children}</body> </html> </ApolloWrapper> ); }
As explained in the documentation for ssr-only-secrets
, calling the function readSSROnlySecret
in a browser environment will always return undefined
. I have modified the approach used by @patrick91
a bit, and I am not sure if this is the best practice, but it seems to work fine for now.
The issue I was having with @patrick91
's approach was that when I tried to use the useSuspenseQuery
hook in a client component, it was not attaching the access token in the headers. This was because client components are rendered on both the server and the client, and readSSROnlySecret
will return undefined
in browser environments.
To handle this issue, I updated the code as follows to make it work on both the server and the client side:
On signIn
, I am storing the access token in cookies as follows:
type TSignIn = { email: string; password: string }
export const signIn = async (params: TSignIn) => {
const result = await getClient().mutate<{ signIn: AuthTokenType }>({
mutation: SIGN_IN_MUTATION,
variables: {
input: {
...params
}
}
})
const accessToken = result.data?.signIn.access_token
cookies().set(authConfig.storageTokenKeyName, accessToken)
}
ApolloWrapper.tsx
'use client'
import { ApolloLink, HttpLink } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
NextSSRApolloClient,
SSRMultipartLink
} from '@apollo/experimental-nextjs-app-support/ssr'
import { readSSROnlySecret } from 'ssr-only-secrets'
import authConfig from './authConfig'
import { getCookie } from '../utils/getCookie'
export function ApolloWrapper({ children, sessionId }: React.PropsWithChildren<{ sessionId: Promise<string> }>) {
function makeClient() {
const authLink = setContext(async (_, { headers }) => {
const cloakedSessionId = await sessionId
const accessToken =
typeof window === 'undefined'
? await readSSROnlySecret(cloakedSessionId, 'SECRET_KEY')
: getCookie(authConfig.storageTokenKeyName)
return {
headers: {
...headers,
authorization: accessToken ? `Bearer ${accessToken}` : ''
}
}
})
const httpLink = new HttpLink({
uri:
typeof window === 'undefined'
? 'https://example.com/graphql'
: `${process.env.NEXT_PUBLIC_SERVER_URL}/graphql`
})
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === 'undefined'
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true
}),
authLink.concat(httpLink)
])
: authLink.concat(httpLink)
})
}
return <ApolloNextAppProvider makeClient={makeClient}>{children}</ApolloNextAppProvider>
}
The issue I was having with
@patrick91
's approach was that when I tried to use theuseSuspenseQuery
hook in a client component, it was not attaching the access token in the headers. This was because client components are rendered on both the server and the client, andreadSSROnlySecret
will returnundefined
in browser environments.
That is pretty much intentional - note that Patrick is working with cookies here. In the browser, cookies would automatically be set by the browser. There's just nothing to do in code here.
But yes, if you're working with manually set headers, you'd have to do it this way. If possible, I'd recommend going cookies only though.
The issue I was having with
@patrick91
's approach was that when I tried to use theuseSuspenseQuery
hook in a client component, it was not attaching the access token in the headers. This was because client components are rendered on both the server and the client, andreadSSROnlySecret
will returnundefined
in browser environments.That is pretty much intentional - note that Patrick is working with cookies here. In the browser, cookies would automatically be set by the browser. There's just nothing to do in code here.
But yes, if you're working with manually set headers, you'd have to do it this way. If possible, I'd recommend going cookies only though.
Yes, in my case, I had to set headers manually because the backend is not configured with cookies. It is configured with Token authentication, so I had to use this approach.
Still stuck with this By logging the sessionid in ApolloWrapper functions
"use client"; import { ApolloLink, HttpLink } from "@apollo/client"; import { setContext } from "@apollo/client/link/context"; import { ApolloNextAppProvider, NextSSRApolloClient, NextSSRInMemoryCache, SSRMultipartLink, } from "@apollo/experimental-nextjs-app-support/ssr"; import { readSSROnlySecret } from "ssr-only-secrets"; const getAuthLink = (cloakedSessionId: Promise<string>) => setContext(async (_, { headers }) => { const sessionId = await readSSROnlySecret( // @ts-ignore await cloakedSessionId, "SECRET_KEY" ); if (sessionId) { return { headers: { // ...(headers || {}), // Cookie: `sessionid=${sessionId}`, ...headers, authorization: sessionId ? `Bearer from wrapper ${sessionId}` : "", }, }; } return { headers }; }); export function ApolloWrapper({ children, sessionId, }: React.PropsWithChildren<{ sessionId: Promise<string> }>) { console.log("🔥🔥🔥🔥🔥🔥🔥🔥", sessionId); function makeClient() { const httpLink = new HttpLink({ uri: typeof window === "undefined" ? "http://localhost:8000/graphql" : "http://localhost:8000/graphql", fetchOptions: { cache: "no-store", crendentials: "include", }, }); return new NextSSRApolloClient({ cache: new NextSSRInMemoryCache(), link: typeof window === "undefined" ? ApolloLink.from([ new SSRMultipartLink({ stripDefer: true, }), getAuthLink(sessionId), httpLink, ]) : httpLink, }); } return <ApolloNextAppProvider makeClient={makeClient}>{children}</ApolloNextAppProvider>; }
I get this
network tab
Apollo Server
Log from apollo server
My env
SECRET_KEY={"alg":"A256CBC","ext":true,"k":"...","key_ops":["encrypt","decrypt"],"kty":"oct"}
From error it seems like your SECRET_KEY
is not valid. Use the function provided in docs to create it. You have used example provided in docs which is not complete.
I generated the secret key and still getting DataError: Invalid key length
The only thing different from my code and docs is
const httpLink = new HttpLink({
uri: typeof window === "undefined" ? "http://localhost:8000/graphql" : "http://localhost:8000/graphql",
fetchOptions: {
cache: "no-store",
crendentials: "include",
},
});
The key cannot be read or is incorrect, this has nothing to do with your HttpLink
.
Can you try to console.log(process.env.SECRET_KEY)
?
Did you copy-paste the example key? That one is deliberately incomplete "..."
I kept my own generated key in SECRET_KEY
as well as in SECRET_KEY_VAR
Strangely while using SECRET_KEY
as env, it shows the example "..."
environment variable, but while using SECRET_KEY_VAR
it shows my generate the key. Now I am using SECRET_KEY_VAR
which is not throwing the error DataError: Invalid key length
Everything working well except the context. The function getAuthLink
is not running either on client or on the server side.. I checked it by doing console.log
const getAuthLink = (cloakedSessionId: Promise<string>) =>
setContext(async (_, { headers }) => {
const sessionId = await readSSROnlySecret(
// @ts-ignore
await cloakedSessionId,
"SECRET_KEY_VAR"
);
console.log("2st Console run");
if (sessionId) {
return {
headers: {
...(headers || {}),
Cookie: `sessionid=${sessionId}`,
},
};
}
return { headers };
});
export function ApolloWrapper({
children,
sessionId,
}: React.PropsWithChildren<{ sessionId: Promise<string> }>) {
function makeClient() {
const httpLink = new HttpLink({
uri: typeof window === "undefined" ? "http://localhost:8000/graphql" : "http://localhost:8000/graphql",
fetchOptions: {
cache: "no-store",
crendentials: "include",
},
});
console.log("1st Console run");
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
getAuthLink(sessionId),
httpLink,
])
: httpLink,
});
}
return <ApolloNextAppProvider makeClient={makeClient}>{children}</ApolloNextAppProvider>;
}
It is also not sending any headers
in apollo server console
In your code snippet
return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
? ApolloLink.from([
new SSRMultipartLink({
stripDefer: true,
}),
getAuthLink(sessionId),
httpLink,
])
: httpLink,
});
}
you run getAuthLink
if you are in SSR (when typeof window === "undefined"
).
You won't see that happening in the browser devtools - but in your NextJs server console (which is probably different from your Apollo Server console)
Now everything seems good, I am following the same code as this https://github.com/apollographql/apollo-client-nextjs/issues/281#issuecomment-2057927433. why the headers are not sent to the Apollo server?
My apollo server
app.use(
"/graphql",
cors({ credentials: true }),
expressMiddleware(server as any, {
context: async ({ req, res }) => {
console.log("req-------------", req.headers);
return {
};
},
})
);
This is what I am getting in the headers of apollo server
I am using JWT http only token. I can't set manually the token. How can I set HTTP only token on apollo client ?
@naimulemon as long as it's the same server, that's fine and it should be sent to your Next server, where you can access it and pass it around with the methods shown in this issue.
If that cookie points to a different server, you have a structural problem that makes SSR of private data impossible - but that doesn't have anything to do with this package or Next.js, more with the general concept of how cookies and the web works.
At that point, you'd have to revisit you authentication architecture, or just stick to in-browser rendering and disable SSR altogether.
Hello! I have existing app react/apollo-graphql/next
I need to ensure login/logout functionality.
My server is on python/django/graphene stack.
I have mutations to login, and refresh the JWT token.
Previously, before SSR i stored got tokens in LocalStorage and then pass into headers of every request:
and refresh token in case of expiration error:
But now I have no localStorage on my SSRed pages. Could you please provide me with a way to manage it in a new way? I use App Router mode and my root layout is with 'use client' directive
Thanks in advance!