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

Client side API query example/explanation #132

Closed
lnikell opened this issue Jun 9, 2020 · 16 comments
Closed

Client side API query example/explanation #132

lnikell opened this issue Jun 9, 2020 · 16 comments

Comments

@lnikell
Copy link

lnikell commented Jun 9, 2020

Thank you for the library first of all! I'm trying to understand some logic that goes behind the scene and for me, it is not clear why the library forces users to have only API query proxied over next.js API.

Auth0 by default allows you to maintain and refresh tokens just using client-side only, however, in the current implementation everything should go over the backend since all functions are available only from the server-side.

So I'm curious how do you propose to handle situations where there is a need to do a query from the client side that requires access token?

@lnikell
Copy link
Author

lnikell commented Jun 9, 2020

Also, usage of auth credentials only works on the backend, I don't understand why there is a need to have all env variables filled during build time('npm run build' command).

@tomclark
Copy link

Also, usage of auth credentials only works on the backend, I don't understand why there is a need to have all env variables filled during build time('npm run build' command).

Don't quite understand that myself, but this post I wrote the other day might be of use if you're building for Docker: #86 (comment). Basically, just create dummy values that get you through the build, and then set env vars as part of your deployment which will overwrite whatever was in your dummy file anyway.

@serendipity1004
Copy link

I need a solution to this problem as well. We are actually using GraphQL as backend and trying to handle Auth0 authentication and a separate Apollo Server. Since the access token is not exposed on the client side, I am not able to use Apollo client to direct query to GraphQL server.

@sandrinodimattia
Copy link
Member

We'll need to write a page explaining your options. This SDK follows the Regular Web Application model, meaning:

  • The application is a confidential client
  • Any tokens should only be consumed by the confidential client (the server side). Tokens are not made available to the client side
  • During SSR, the user is available on the server side

If your need is to have access tokens on the client and calling APIs from the client I would suggest moving to our new React SDK: https://auth0.com/docs/quickstart/spa/react/01-login

@lucas-janon
Copy link

@sandrinodimattia can the two solutions be used together? We need both server-side and client-side authentication.

@mosesoak
Copy link

@sandrinodimattia Not to pile on, I think I understand what you're saying based on reading your invaluable ultimate guide article.

That article was written a while back and the React SDK is just coming out now with their own Next JS integration example. But my guess is that if you were to update that article today (not a bad idea?) you would say to use the React SDK in order to take the first option, Serverless With the User on the Frontend, but continue to use this nextjs-auth0 SDK to take the Serverless With the User on the Backend option, if you're able to proxy all calls through api endpoints, is that correct?

Thank you again, your explanation in that article is so incredibly thorough and helpful, it's really a godsend! 🙏

@danthareja
Copy link

danthareja commented Aug 27, 2020

I also had this need and wanted to share my solution to this. For my use-case, I wanted to be able to:

  1. Selectively decide which pages to authenticate with a Higher Order Component
  2. Use Auth0's accessToken to make authenticated requests to an External API, from pages during both server side and client side rendering, without proxying the request to an /api page
  3. Avoid sending a re-authenticating network request if a page was requested client-side with the Link component

If this is useful, I'd be happy to turn it into a PR.

Credit: this was adapted from an old with-redux implementation

Code

IMPORTANT: The current implementation assumes you have a working login route at pages/api/login. If it's somewhere else, you'll have to update this currently hardcoded value throughout the example utils/session-provider.js file

pages/dashboard.js
import React from 'react'

import { withSession, useSession } from '../utils/session-provider'

function DashboardPage(props) {
  const session = useSession()

  console.log(props.hello) // 'hello from getInitialProps'
  
  // You can use `session.accessToken` to make an authenticated request on the client
  // Could be with useEffect, or your own useApi, useQuery, or whatever
  const data = useApi('/dashboard', session.accessToken)

  return (
    <Dashboard data={data} />
  )
}

DashboardPage.getInitialProps = async function (context) {
  const { session } = context;
  
  // You can use `session.accessToken` to make an authenticated request on first page load
  // This works for both server and client requests
  // Here, you'd have to use `fetch` or `axios`, or whatever else directly.
  const data = await fetch('/api/dashboard', session.accessToken)

  return {
    message: 'hello from getInitialProps',
  }
}

export default withSession(DashboardPage)
utils/auth0.js
import { initAuth0 } from '@auth0/nextjs-auth0'

export default initAuth0({
 // ... your config
  session: {
   // ... your session config
    storeAccessToken: true, // required for this example to work
    storeRefreshToken: true, // required for this example to work
  }
})
utils/session-provider.js
import React from 'react'
import App from 'next/app'
import Router from 'next/router'
import auth0 from './auth0'

const SessionContext = React.createContext()
SessionContext.displayName = 'SessionContext'

export function useSession() {
  const context = React.useContext(SessionContext)
  if (context === undefined) {
    throw new Error(
      'useSession must be used within a SessionProvider. Did you wrap the page component with `withSession`?'
    )
  }
  return context
}

export const withSession = (PageComponent) => {
  const WithSession = ({ session, ...props }) => {
    return (
      <SessionContext.Provider value={session}>
        <PageComponent {...props} />
      </SessionContext.Provider>
    )
  }

  WithSession.getInitialProps = async (context) => {
    const session = await getSession(context)

    // Provide the session to getInitialProps of pages
    // In order to make authenticated requests
    context.session = session

    // Run getInitialProps from HOCed PageComponent
    const pageProps =
      typeof PageComponent.getInitialProps === 'function'
        ? await PageComponent.getInitialProps(context)
        : {}

    // Pass props to PageComponent
    return {
      session,
      ...pageProps,
    }
  }

  return WithSession
}

async function getSession(context) {
  const { req, res, pathname } = context

  // On the server, we can ask auth0 for the session
  if (req) {
    const session = await auth0.getSession(req)
    if (!session || !session.user) {
      res.writeHead(302, {
        Location: '/api/login',
      })
      res.end()
      return {}
    } else {
      const tokenCache = auth0.tokenCache(req, res)
      const { accessToken } = await tokenCache.getAccessToken()
      return {
        ...session,
        accessToken,
      }
    }
  }

  // On the client
  const { session } = __NEXT_DATA__.props.pageProps
  if (session) {
    // If we go from authenticated to authenticated page (both using withSession),
    // We can pass around the session from pageProps
    // This crucially avoids an un-needed network request when a page is requested from a Link component
    return session
  } else {
    // If we go from unauthenticated (without withSession) to authenticated page (with withSession),
    // we have to redirect through login first
    Router.replace(`/api/login?redirectTo=${pathname}`)
  }
}

@mosesoak
Copy link

@danthareja Couple of things with your example code: first, it's a little more straightforward to destructure the user from the session and provide that directly as a prop. The frontend doesn't need and probably should not get the entire session object with all of its cookie information. Second, I'm pretty sure getInitialProps is being outmoded by next and you should use getServerSideProps. Finally I'm not sure the use of __NEXT_DATA__.props.pageProps is encouraged as best practice.

The way I solved what you're talking about is this:

  • _app does not call any server side functions to avoid locking to one modality
  • per page, getServerSideProps calls a shared function that gets the cached logged-in session, if there is one
  • _app also looks for user showing up in props. If it's not found, we may be rendering a static SSG page that doesn't contain logged-in information. I've written a hook for this that accepts a potential user prop and consumes that if it's there, then if not it calls api endpoint that looks up the session. If one is found we're logged in and the user is hydrated almost instantly after load for SSG. If not logged in, a local ref in the hook remembers that we're logged out, in order to not keep trying this on every _app render.

This system works great so far, in that it keeps all the cookie and session logic on the backend (including secrets obv.) and then automates providing user to all SSG pages a few milliseconds after page load, when it's available. Now all my pages can assume that user will exist when we're logged in, possibly not on first render but very shortly thereafter, which vastly simplifies writing the frontend.

@danthareja
Copy link

danthareja commented Aug 27, 2020

Thanks for the reply, @mosesoak! I appreciate the feedback.

Good call on destructuring the user, I like that suggestion. The original intention of the whole session was to include any additional properties added by the onUserLoaded callback, but after looking at that spec again, you can add props to the user directly.

I actually chose to use getInitialProps intentionally over getServerSideProps to handle the requirement of not sending an extra request during client-side page transitions. With getServerSideProps, a request is sent on client-side page transitions, and I figured that since the authentication was handled on the first server-side render of the page, it didn't make sense to send this additional request (like how auth0-react does it).

Your setup sounds interesting. Where are you caching the logged-in session that you lookup in getServerSideProps? Is this something else in addition to auth0.getSession?

Could you share an example of the custom hook?

Edit: I 100% agree using __NEXT_DATA__ feels dirty, but I didn't know how else to share session data across client-side page transitions.

@mosesoak
Copy link

@danthareja It's already cached in the cookie. Lookup of the session doesn't necessarily call the server.

My hook is really similar to what you'd find in Sandrino's example code, except that I add the logged-out state which can be boolean or undefined (initial/unset). Here's a simplified example version.

// for use in _app only
export const useFetchUser = (ssrUser?: User): User => {
  if (isServerSide) {
    return ssrUser;
  }
  const [user, setUser] = useState(ssrUser);
  const [isFetchingUser, setIsFetching] = useState(false);
  const [isLoggedOut, setIsLoggedOut] = useState(ssrUser ? false : undefined);

  useEffect(() => {
    if (isLoggedOut === undefined && !isFetchingUser && !user) {
      setIsFetching(true);
      fetchUser().then((res) => {
        setIsFetching(false);
        if (res) {
          setUser(processUser(res));
        } else {
          setIsLoggedOut(true);
        }
      });
    }
  }, []);

  return { user, isFetchingUser };
};

@danthareja
Copy link

Lookup of the session doesn't necessarily call the server.

Isn't this implied by using getServerSideProps? From the docs:

When you request this page on client-side page transitions through next/link (documentation) or next/router (documentation), Next.js sends an API request to the server, which runs getServerSideProps

How would you avoid this extra request in your example? Maybe it's not necessary and I'm overthinking this whole thing... I'm admittedly new to the Next.js patterns.

@mosesoak
Copy link

Oh, you mean the nextjs api route? Yes it would call that, I was referring to not calling auth0's server. I don't think there's much of a penalty for using api routes in next, they're screamingly fast and a great way to handle stuff like this. This nextjs-auth0 library is really authored to be used as much within api routes as possible.

@danthareja
Copy link

Yeah, I was thinking of a way to avoid that request. I only SSR a UI skeleton (including a user profile), and most of my data fetching is done in the client as an SPA, so I wanted to keep those transitions snappy. I haven't profiled the performance of using getServerSideProps, and this could very well be an over-optimization.

Thanks for sharing your thoughts @mosesoak, it's always nice to hear perspectives from others.

@hems
Copy link

hems commented Nov 12, 2020

Oh, you mean the nextjs api route? Yes it would call that, I was referring to not calling auth0's server. I don't think there's much of a penalty for using api routes in next, they're screamingly fast and a great way to handle stuff like this. This nextjs-auth0 library is really authored to be used as much within api routes as possible.

I been also wondering if there isn't a way of reading the accessToken from the client side without doing a request to the next.js API asking for the token.

But from my understanding the only way would be to get the accessToken on getServerSideProps and send it to the frontend as a prop, right?

Also with the data available on the client side, there is no way a user could sign a request and send it signed as an alternative to using the accessToken ?

Also i'm a bit unsure about how the refreshToken flow happens in this context.

If the token expires the the API should notify the user with an error and then the user must call next.js api to get a new accessToken and retry the API call with the newly refreshed accessToken ?


TLDR:

Please correct me if i'm wrong, but from my understanding on this thread, there is no way of :

  1. Obtaining the accessToken on the client side, using auth0 library on it's own after the user is logged in without calling an endpoint on next.js
  2. There is no way of signing a request on the client side and then verifying it was signed by the user on the backend as an alternative to sending the accessToken to the server?

@mosesoak
Copy link

Right, however this library is very much designed as a serverless-driven alternative to their other frontend-centric libs. Since you want to use frontend I'd suggest using their js client (or the react one), which are able to fetch access tokens and have the advantage of being able to silently refresh user login using a hidden iframe.

@Widcket
Copy link
Contributor

Widcket commented Jan 15, 2021

Hi everyone, please check out the examples page of the new v1.0.0-beta.0 release for guidance on this:

Also, we added some information in the README of the beta wrt this library vs auth0-react.

@Widcket Widcket closed this as completed Jan 15, 2021
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

9 participants