Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

examples handling authenticated clients #21

Open
masterkain opened this issue May 11, 2023 · 32 comments
Open

examples handling authenticated clients #21

masterkain opened this issue May 11, 2023 · 32 comments

Comments

@masterkain
Copy link

this has been a torn in my side for a while, if you can provide an example with authenticated clients that would be super.

@patrick91
Copy link
Contributor

@masterkain that's a good suggestion! I think I can update the polls demo to add a section that needs authentication :)

Do you have any preference regarding auth system? I'll try with a cookie based session if not 😊

@masterkain
Copy link
Author

cookies / auth token would be a blessing for me, need to better understand what to do when a client becomes unauthenticated, how to properly pass auth data to the client after login, etc. ❤️

@seanaguinaga
Copy link

This library is amazing for firebase

https://github.com/awinogrodzki/next-firebase-auth-edge

easy examples

@seanaguinaga
Copy link

seanaguinaga commented May 11, 2023

I just have it doing this now

layout.tsx

import { getTokens } from 'next-firebase-auth-edge/lib/next/tokens';
import { cookies } from 'next/dist/client/components/headers';
import { ApolloWrapper } from '../components/apollo-wrapper';
import { AuthProvider } from '../components/auth-provider';
import { mapTokensToTenant } from '../lib/firebase/auth';
import { serverConfig } from '../lib/firebase/server-config';

import './global.css';

export const metadata = {
  title: 'Nx Next App',
  description: 'Generated by create-nx-workspace',
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const tokens = await getTokens(cookies(), {
    serviceAccount: serverConfig.serviceAccount,
    apiKey: serverConfig.firebaseApiKey,
    cookieName: 'AuthToken',
    cookieSignatureKeys: ['secret1', 'secret2'],
  });

  const tenant = tokens ? mapTokensToTenant(tokens) : null;

  return (
    <html lang="en">
      <body>
        <AuthProvider defaultTenant={tenant}>
          <ApolloWrapper token={tokens?.token}>{children}</ApolloWrapper>
        </AuthProvider>
      </body>
    </html>
  );
}

apollo-wrapper.tsx

'use client';

import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  SuspenseCache,
} from '@apollo/client';
import {
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  SSRMultipartLink,
} from '@apollo/experimental-nextjs-app-support/ssr';
import React from 'react';

const uri = process.env.NEXT_PUBLIC_HASURA_URL;

function createClient(token: string | undefined) {
  const httpLink = new HttpLink({
    uri,
    headers: token
      ? {
          Authorization: `Bearer ${token}`,
        }
      : {
          'x-hasura-admin-secret': 'myadminsecretkey',
        },
  });

  return new ApolloClient({
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === 'undefined'
        ? ApolloLink.from([
            new SSRMultipartLink({
              stripDefer: true,
            }),
            httpLink,
          ])
        : httpLink,
  });
}

function makeSuspenseCache() {
  return new SuspenseCache();
}

export function ApolloWrapper({
  children,
  token,
}: React.PropsWithChildren<{
  token: string | undefined;
}>) {
  const makeClient = React.useCallback(() => createClient(token), [token]);

  return (
    <ApolloNextAppProvider
      makeClient={makeClient}
      makeSuspenseCache={makeSuspenseCache}
    >
      {children}
    </ApolloNextAppProvider>
  );
}

@phryneas
Copy link
Member

Yup, that's a very good approach!

Just to highlight the important parts from your code snippet to make it easier for other people following along:

  • use cookies() from 'next/dist/client/components/headers' in a React Server Component to get the current cookies, and extract your token from them
  • pass that token as a prop into your ApolloWrapper.
  • use that token in your makeClient function

@louisthomaspro
Copy link

Thanks @seanaguinaga! How do you update the given token when it expired ?

@rafaelsmgomes
Copy link

rafaelsmgomes commented May 30, 2023

Any examples of doing this with Next-Auth?

Also, the example above is for client-side auth. Is there a way to use authentication with RSC?

Do we need to manually pass the context into every call?

We used to pass the context into the createIsomorphicLink function like so:

type ResolverContext = {
  req?: IncomingMessage
  res?: ServerResponse
}

function createIsomorphicLink(context?: ResolverContext) {
  if (typeof window === 'undefined') {
    const { SchemaLink } = require('@apollo/client/link/schema')

    const schema = makeExecutableSchema({
      typeDefs: typeDefs,
    })
    return new SchemaLink({ schema, context })
  } else {
    const { HttpLink } = require('@apollo/client')
    return new HttpLink({
      uri: '/api/graphql',
      credentials: 'same-origin',
    })
  }
}

Is there a way we can do this in the getClient function to have some context on the Server Side calls?

Can we use SchemaLink with getClient?

@phryneas
Copy link
Member

phryneas commented May 31, 2023

@rafaelsmgomes You probably don't need context here, in Next.js you should be able to just call headers() or cookies() within your registerApolloClient callback.

Can we use SchemaLink with getClient?

I don't see a reason why not, but be aware that if you have any global variables like typeDefs here, they will be shared between all requests, so don't store any state in there.

@seanaguinaga
Copy link

Thanks @seanaguinaga! How do you update the given token when it expired ?

The auth library does that for me, thankfully

@rafaelsmgomes
Copy link

Hi, @phryneas!

I still don't understand how to crack this one.

This is how I used to authenticate the getServerSideProps function:

export async function getServerSideProps(ctx: GetServerSidePropsContext<{ symbol: string }>) {
  const { symbol } = ctx.params!
  const { req, res } = ctx

  const apolloClient = initializeApollo({})

  const context: DefaultContext = {
    headers: {
      cookie: req.headers?.cookie, // this is where I'm passing the cookies down to authenticate
    },
  }

  await apolloClient.query({
      query: GET_PROFILE_CHART,
      variables: { symbol, fromDate },
      context,
    }),

  return addApolloState(apolloClient, {
    props: { symbol },
  })
}

I'm trying to authenticate my user on the register client function calls via:

import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc'
import { cookies } from 'next/dist/client/components/headers'

export const { getClient } = registerApolloClient(() => {

  let nextCookies = cookies()
    .getAll()
    .reduce((acc, cur) => {
      const { name, value } = cur
      acc += `${name}:${value}`
      return acc
    }, '')
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      // https://studio.apollographql.com/public/spacex-l4uc6p/
      // uri: '/api/graphql',
      uri: 'http://localhost:3000/api/graphql',
      headers: {
        cookie: nextCookies,
      },
      credentials: 'same-origin',

      // you can disable result caching here if you want to
      // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
      // fetchOptions: { cache: "no-store" },
    }),
  })
})

That did not work, but neither did passing the context in the getClient function:

  let nextCookies = cookies()
    .getAll()
    .reduce((acc, cur) => {
      const { name, value } = cur
      acc += `${name}:${value}`
      return acc
    }, '')

  const { data } = await getClient().query({
    query: TEST_QUERY,
    variables: { symbol },
    context: { headers: { cookie: nextCookies } },
  })

I thought this would work, and I don't see another way of doing it.

Maybe I added the cookies in a wrongful way? But that would mean the headers need to be passed differently now?

Maybe I have to call cookies in a different way. Or not pass it in the same manner as it works in the frontend. I don't know.

@phryneas
Copy link
Member

phryneas commented Jun 9, 2023

@rafaelsmgomes It feels to me that both of these should be working - have you tried to console.log that nextCookies variable?

@rafaelsmgomes
Copy link

Hi @phryneas! Thanks for letting me know as I pursuing the right solution!

The issue was within the keyboard and the chair on my side of the screen.
The reduce function had a couple of mistakes!

I'll put it here in case anyone is wondering:

  let nextCookies = cookies()
    .getAll()
    .reduce((acc, cur, i, arr) => {
      const { name, value } = cur
      acc += `${name}=${value}${arr.length - 1 !== i ? '; ' : ''}` // forgot to give it a space after each cookie. Also, was using ":" instead of "="
      return acc
    }, '')

@jnovak-SM2Dev
Copy link

Is there an example of how to use this with Next Auth? I'm having a lot of issues with getting this to work in both client and server side components.

@yasharzolmajdi
Copy link

Yup, that's a very good approach!

Just to highlight the important parts from your code snippet to make it easier for other people following along:

  • use cookies() from 'next/dist/client/components/headers' in a React Server Component to get the current cookies, and extract your token from them
  • pass that token as a prop into your ApolloWrapper.
  • use that token in your makeClient function

One issue I'm having is that, my login page is part of the same app.
when makeClient is called with user A token and then during the same session you log out and login with user B. makeClient will not get called again with the new token.

my flow is

  1. User visits "/login"
  2. Login with Credentials A and cookie gets set by external API
  3. Navigate user to dashboard using next/navigation
  4. Log out and navigate to login using next/navigation
  5. Login with Credentials B and cookie gets set by external API
  6. Navigate user to dashboard using next/navigation

The dashboard information is still showing information from Credentials A. The components are mix of "use client" and "use server". When console logging the token it only ever gets set on when makeClient is called, which only happens once.

as a work around for now I do window.location.href = "/dashbaord"; and window.location.href = "/login"; when navigating between private and public pages so that makeClient gets called with the correct token.

any ideas what i might be doing wrong or solution for this issue?

@p1pah
Copy link

p1pah commented Aug 25, 2023

Looking for some help on this myself, I am just trying to send in a new token/create a new authorization header when a user logins. My setToken function gets called when a successful login comes back, I noticed my makeClient was never being called again...Any help would be greatly appreciated! :D

export const Wrapper = ({children}: {children: React.ReactNode}) => {
  const [token, setToken] = React.useState<string>()
  const makeClient = React.useCallback(() => createClient(token), [token])
  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      <ToastProvider>
        <UserProvider setToken={setToken}>{children}</UserProvider>
      </ToastProvider>
    </ApolloNextAppProvider>
  )
}

@chvllad
Copy link

chvllad commented Aug 25, 2023

Seems like currently the only way is to save token in local storage/cookies and reload page. ApolloNextAppProvider specifically creates singleton and calls makeClient once in server context and once in client one.
https://github.com/apollographql/apollo-client-nextjs/blob/main/package/src/ssr/ApolloNextAppProvider.tsx

@phryneas
Copy link
Member

phryneas commented Aug 28, 2023

@ben-hapip @chvllad You should never recreate your whole ApolloClient instance when an authentication token changes.

The best way to solve this would storing the auth token in a way that is transparent to Apollo Client (at least in the browser) - in a httpOnly secure cookie.
If that is not possible, you could e.g. use a ref to hold your token, inline your makeClient function and access that ref from your setContext link to add the authentication header.

@p1pah
Copy link

p1pah commented Aug 28, 2023

Ayyy thanks fellas for the suggestions!! 🤝

@romain-hasseveldt
Copy link

Looking for some help on this myself, I am just trying to send in a new token/create a new authorization header when a user logins. My setToken function gets called when a successful login comes back, I noticed my makeClient was never being called again...Any help would be greatly appreciated! :D

Has this been solved @ben-hapip ? I tried your solution @phryneas but it does not work for me :(

@phryneas
Copy link
Member

phryneas commented Sep 13, 2023

@romain-hasseveldt As I said before, makeClient will not be called again, and you should also never do that in the browser. You should keep one Apollo Client instance for the full lifetime of your application, or you will throw your full cache away.

If you show a code example here, I can show you how to leverage a ref here to change a token without recreating the client.

@romain-hasseveldt
Copy link

Hello @phryneas , thank you for the promptness of your response. Here is what my current implementation looks like:

'use client';

import { ApolloLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import {
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  NextSSRApolloClient,
  SSRMultipartLink,
} from '@apollo/experimental-nextjs-app-support/ssr';
import { createUploadLink } from 'apollo-upload-client';
import { User } from 'next-auth';
import { useSession } from 'next-auth/react';

function makeClient(user?: User) {
  const httpLink = createUploadLink({
    uri: process.env.NEXT_PUBLIC_BACK_GRAPHQL_URL,
  }) as unknown as ApolloLink;

  const authLink = setContext((_, { headers }) => {
    return {
      headers: {
        ...headers,
        authorization: user ? `Bearer ${user.jwt}` : '',
      },
    };
  });

  const links = [authLink, httpLink];

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === 'undefined'
        ? from(
            [
              new SSRMultipartLink({
                stripDefer: true,
              }),
              links,
            ].flat(),
          )
        : from(links),
  });
}

export function ApolloWrapper({ children }: React.PropsWithChildren) {
  const { data: session } = useSession();

  return (
    <ApolloNextAppProvider makeClient={() => makeClient(session?.user)}>
      {children}
    </ApolloNextAppProvider>
  );
}

@phryneas
Copy link
Member

phryneas commented Sep 13, 2023

In that case, you need to move makeClient into the scope of your ApolloWrapper and use a ref to keep updating your scope-accessible session:

export function ApolloWrapper({ children }: React.PropsWithChildren) {
  const { data: session } = useSession();
  const sessionRef = useRef(session);
  useEffect(() => {
    sessionRef.current = session;
  }, [session])

  function makeClient() {
    const httpLink = createUploadLink({
      uri: process.env.NEXT_PUBLIC_BACK_GRAPHQL_URL,
    }) as unknown as ApolloLink;

    const authLink = setContext((_, { headers }) => {
      return {
        headers: {
          ...headers,
          authorization: sessionRef.current.user ? `Bearer ${sessionRef.current.user.jwt}` : "",
        },
      };
    });

    const links = [authLink, httpLink];

    return new NextSSRApolloClient({
      cache: new NextSSRInMemoryCache(),
      link:
        typeof window === "undefined"
          ? from(
              [
                new SSRMultipartLink({
                  stripDefer: true,
                }),
                links,
              ].flat()
            )
          : from(links),
    });
  }

  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

@romain-hasseveldt
Copy link

It does not seem to work (at least in my case). The value of sessionRef as I retrieve it in setContext is always null, which corresponds to the initial value passed to useRef, even though it later gets updated in the useEffect. Do you have any idea what the issue might be? Thanks again for your help!

@phryneas
Copy link
Member

And you're actually accessing sessionRef.current and not destructuring something somewhere?

One correction though:

-          authorization: user ? `Bearer ${sessionRef.current.user.jwt}` : "",
+          authorization: sessionRef.current.user ? `Bearer ${sessionRef.current.user.jwt}` : "",

@romain-hasseveldt
Copy link

This is the current state of my implementation:

export function ApolloWrapper({ children }: React.PropsWithChildren) {
  const { data: session } = useSession();
  const sessionRef = useRef(session);
  useEffect(() => {
    sessionRef.current = session;
  }, [session]);

  function makeClient() {
    const httpLink = createUploadLink({
      uri: process.env.NEXT_PUBLIC_BACK_GRAPHQL_URL,
    }) as unknown as ApolloLink;

    const authLink = setContext((_, { headers }) => {
      return {
        headers: {
          ...headers,
          authorization: sessionRef.current?.user
            ? `Bearer ${sessionRef.current?.user.jwt}`
            : '',
        },
      };
    });

    const links = [authLink, httpLink];

    return new NextSSRApolloClient({
      cache: new NextSSRInMemoryCache(),
      link:
        typeof window === 'undefined'
          ? from(
              [
                new SSRMultipartLink({
                  stripDefer: true,
                }),
                links,
              ].flat(),
            )
          : from(links),
    });
  }

  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

@phryneas
Copy link
Member

And if you add some console.log calls here:

  useEffect(() => {
    console.log("updating sessionRef to", session
    sessionRef.current = session;

and here

    const authLink = setContext((_, { headers }) => {
      console.log("working with", sessionRef.current)
      return {

what log(and which order) do you get?

@romain-hasseveldt
Copy link

I have the following:

  1. updating sessionRef to session object
  2. working with null

@jerelmiller
Copy link
Member

jerelmiller commented Sep 13, 2023

Hey @romain-hasseveldt 👋

Could you try assigning the sessionRef on every render instead of inside a useEffect? This should keep it in sync with the latest value since effects fire after render (which means that session will always "lag behind" a bit)

Try this:

Old code suggestion
const sessionRef = useRef(session);

// assign on every render to keep it up-to-date with the latest value
sessionRef.current = session;

Edit: Apparently React deems this as unsafe, which is something I did not know about until now. Please ignore my suggestion 🙂

This is also mentioned in the useRef docs
Screenshot 2023-09-13 at 11 24 40 AM

@romain-hasseveldt
Copy link

Thank you for trying @jerelmiller :) I tested out of curiosity, and... it doesn't work either.

@phryneas
Copy link
Member

This is honestly weird - could you try to create a small reproduction of that?

@wchorski
Copy link

wchorski commented Oct 6, 2023

So I believe I got token auth working for use client components via

app/ApolloWrapper.tsx

"use client";
// ^ this file needs the "use client" pragma
// https://github.com/apollographql/apollo-client-nextjs

import { ApolloLink, HttpLink } from "@apollo/client";
import {
  ApolloNextAppProvider,
  NextSSRInMemoryCache,
  NextSSRApolloClient,
  SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { envvars } from "@lib/envvars";

// have a function to create a client for you
function makeClient(token:string|undefined) {
  const httpLink = new HttpLink({
    // this needs to be an absolute url, as relative urls cannot be used in SSR
    uri: envvars.API_URI,
    headers: {
      Authorization: token ? `Bearer ${token}` : '',
    },
  
    // you can disable result caching here if you want to
    // (this does not work if you are rendering your page with `export const dynamic = "force-static"`)
    fetchOptions: { cache: "no-store" },
    // you can override the default `fetchOptions` on a per query basis
    // via the `context` property on the options passed as a second argument
    // to an Apollo Client data fetching hook, e.g.:
    // const { data } = useSuspenseQuery(MY_QUERY, { context: { fetchOptions: { cache: "force-cache" }}});
  });

  return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            httpLink,
          ])
        : httpLink,
  });
}

// you need to create a component to wrap your app in
export function ApolloWrapper({ token, children }: React.PropsWithChildren<{
  token: string | undefined;
}>) {
  return (
    <ApolloNextAppProvider makeClient={() => makeClient(token)}>
      {children}
    </ApolloNextAppProvider>
  );
}

I'm also trying to figure out auth via Server side client.ts script

client.ts

import { HttpLink } from "@apollo/client";
import {
  NextSSRInMemoryCache,
  NextSSRApolloClient,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";
import { envs } from "@/envs";
import { cookies } from "next/headers";

export const { getClient } = registerApolloClient(() => {

  const cookieStore = cookies()
  const cookieSession = cookieStore.get('keystonejs-session')
  console.log('cookieSession::::: ');
 const token = cookieSession?.value
 console.log(`==== Bearer ${token}`);
  

  return new NextSSRApolloClient({
    cache: new NextSSRInMemoryCache(),
    link: new HttpLink({
      uri: envs.API_URI,
    }),
    headers: {
      'Authorization': (token) ? `Bearer ${token}`: "",
      'Content-Type': 'application/json'
    },
  })
})

I know I'm getting the right session token because It works when I manually set the token in Apollo Studio Sandbox it works, but If I manually set the token inside this client.ts it still doesn't work.

a linke to the full source code git repo

@eavelasquez
Copy link

eavelasquez commented Jul 22, 2024

Hi everyone,

I'm encountering an issue with Apollo Client and Auth.js in my Next.js application. The problem is that when making GraphQL requests, the cookies are being overwritten by Auth.js, causing my API to respond that the necessary authorization cookie was not sent.

Current setup:

  • Next.js application (app router)
  • Using Apollo Client for GraphQL requests
  • Auth.js for authentication

The issue:
When Apollo Client makes a request, it seems that Auth.js is overwriting or removing the cookies I'm trying to set for authorization on the client side. As a result, my API is failing to authorize the requests.

I've tried several approaches, however, these attempts haven't resolved the cookie conflict with Auth.js on the client side.

Questions:

  1. Is there a known issue or best practice for using Apollo Client alongside Auth.js?
  2. How can I ensure that my authorization cookies are properly sent with Apollo Client requests without being overwritten by Auth.js?
  3. Are there any specific Apollo Client configuration options I should be aware of to prevent this cookie conflict?

Any insights or suggestions would be greatly appreciated. If you need any additional information about my setup, please let me know.

Here's my Apollo Client configuration:

'use client';

import { type PropsWithChildren } from 'react';
import { ApolloLink, HttpLink } from '@apollo/client';
import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { setContext } from '@apollo/client/link/context';
import {
  ApolloClient,
  ApolloNextAppProvider,
  InMemoryCache,
  SSRMultipartLink,
} from '@apollo/experimental-nextjs-app-support';
import { useSession } from 'next-auth/react';

if (process.env.NODE_ENV === 'development') {
  loadDevMessages();
  loadErrorMessages();
}

export function ApolloWrapper({ children }: PropsWithChildren) {
  const { data: session } = useSession();

  const makeClient = () => {
    // Create an HTTP link to the GraphQL API
    const httpLink = new HttpLink({
      uri: process.env.NEXT_PUBLIC_API_URL,
      fetchOptions: { cache: 'no-store' },
      credentials: 'include',
    });

    // Create an authentication link to set the appropriate headers
    const authLink = setContext((_, { headers }) => {
      return {
        headers: {
          ...headers,
          cookie: headers?.cookie.concat(
            session
              ? `access-token=${session?.access_token}; refresh-token=${session?.access_token}`
              : '',
          ),
        },
      };
    });

    // Create the links for the client and server
    const clientLinks = [authLink, httpLink];
    const ssrLinks = [
      new SSRMultipartLink({ stripDefer: true }),
      ...clientLinks,
    ];

    // Create the Apollo Client
    return new ApolloClient({
      cache: new InMemoryCache(),
      link: ApolloLink.from(
        typeof window === 'undefined' ? ssrLinks : clientLinks,
      ),
    });
  };

  if (!session) return null;

  return (
    <ApolloNextAppProvider makeClient={makeClient}>
      {children}
    </ApolloNextAppProvider>
  );
}

Thank you in advance for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests