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

Unable to attach Auth0 access token to headers inside Apollo client #100

Closed
yhbk12 opened this issue Sep 23, 2023 · 6 comments
Closed

Unable to attach Auth0 access token to headers inside Apollo client #100

yhbk12 opened this issue Sep 23, 2023 · 6 comments

Comments

@yhbk12
Copy link

yhbk12 commented Sep 23, 2023

Details shown here. Would love to get some help here, been struggling with this for a while now.

https://stackoverflow.com/questions/77164538/attach-auth0-access-token-as-request-header-for-graphql-apolloclient-in-nextjs-a

@mvandergrift
Copy link

mvandergrift commented Sep 23, 2023

It appears that the Auth0 code you're using [getSession, getAccessToken] is designed to run on the server. Even if you could await it, I don't think it would work. Instead of trying to retrieve the AT in the ApolloProvider, I would grab the access token in the application root (layout.tsx) and pass it down through the props to the ApolloProvider.

// layout.tsx
import { getAccessToken } from "@auth0/nextjs-auth0";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
    const token = await getAccessToken(); 

    return (
        <html lang="en">
            <body>
                <UserProvider>
                    <ApolloWrapper token={token} >
                            {children}                         
                    </ApolloWrapper>
                </UserProvider>
            </body>
        </html>
    );
}

Your ApolloProvider now has access to the AT, which you can use in your authLink.

This works well until the AT expires. Since the code in the root only runs once, you never get a chance to call getAccessToken again to use your refresh token. I ended up passing the AT expiration down to the ApolloProvider as well, checking for that in authLink, and then doing a router.refresh() if needed (which executes getAccessToken). This seems ugly, and hopefully, someone has a better way to handle this.

This is a stripped-down version of the ApolloProvider:

"use client";
import {
    ApolloLink,
    HttpLink
} from "@apollo/client";

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

import { onError } from "@apollo/client/link/error";
import { RetryLink } from "@apollo/client/link/retry";

import { useRouter } from "next/navigation";

const ApolloWrapper = ({ token, children }) => {
    const router = useRouter();
    const httpLink = new HttpLink({
        uri: process.env.API_URL
    });

    const authLink = new ApolloLink((operation, forward) => {
        if (token?.accessToken) {
            try {
                const expireDate = new Date(token.expiresAt * 1000);
                if (expireDate < new Date()) {
                    console.debug("[GraphQL debug] Access token expired, refreshing:", expireDate);
                    router.refresh();
                }

                operation.setContext(({ headers }) => ({
                    headers: {
                        authorization: `Bearer ${token?.accessToken}`,
                        ...headers
                    }
                }));
            } catch (error) {
                console.log(error);
            }
        }

        return forward(operation);
    });

    const authHttpLink = ApolloLink.from([
        onError(({ graphQLErrors, networkError }) => {
            if (graphQLErrors) {
                graphQLErrors.forEach(({ message, locations, path }) =>
                    console.error(
                        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
                    )
                );
            }
            if (networkError) {
                console.error("[Network error]:", networkError);
            }
        }),
        authLink,
        new RetryLink(),
        typeof window === "undefined"
            ? ApolloLink.from([
                new SSRMultipartLink({
                    stripDefer: false,
                    cutoffDelay: 200,
                }),
                httpLink,
            ])
            : httpLink
    ]);


    const makeClient = () => (
        new NextSSRApolloClient({
            name: "web-ssr",
            version: "1.2",
            link: authHttpLink,
            cache: new NextSSRInMemoryCache()
            //connectToDevTools: devTools,
        })
    );

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

export default ApolloWrapper;

The full version of the app root, including the token expiration. I couldn't find a way to get this from Auth0, so I needed to parse the token to get it.

import { getAccessToken, getSession } from "@auth0/nextjs-auth0";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
    const session = await getSession();
    let token;

    if (session) {
        const at = await getAccessToken();

        if (at?.accessToken) {            
            const decoded = jwt_decode(at.accessToken) as { exp: number };

            token = {
                accessToken: at.accessToken,
                accessTokenDecoded: decoded,
                user: session.user,
                expiresAt: decoded.exp
            };
        }
    }


    return (
        <html lang="en">
            <body>
                <UserProvider>
                    <ApolloWrapper token={token} >
                        {children}
                    </ApolloWrapper>
                </UserProvider>
            </body>
        </html>
    );
}

@yhbk12
Copy link
Author

yhbk12 commented Sep 24, 2023

Thanks for the response @mvandergrift. Unfortunately whenever I try to make my _app.js, or layout.js functions async, I get this error:

"Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead."

Any idea why this happens? Seems like I'm unable to make anything async.

image

@mvandergrift
Copy link

It looks like you've built your project in the /pages directory instead of the /app directory, so you're not using the AppRouter. Async components are only available within the AppRouter framework. Hopefully, it's as easy as moving some files over and renaming a couple of pages. Otherwise, Vercel has a great guide on migration to the app router: https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration

@phryneas
Copy link
Member

I'm doing some housekeeping so I'm closing some older issues that haven't seen activity in a while.
If this is still relevant, please feel free to reopen the issue.

Copy link
Contributor

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.

@tomoima525
Copy link

FYI: As far as I read from Auth0 community discussion, getAccessToken() refreshes the access token internally hence you don't need to handle the refresh logic https://community.auth0.com/t/how-to-use-the-refresh-token-with-auth0-nextjs-auth0/126612

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

4 participants