next-auth: useSession({ required: true }), which always returns a non-null Session object

Summary of proposed feature

A living session could be a requirement for specific pages. If it doesn’t exist, then the user should be redirected to the sign in page with an error like “Session expired, please try signing in again”.

Purpose of proposed feature

Sometimes, a user might log out by accident, or by deleting cookies on purpose. If that happens (e.g. on a separate tab), then useSession({ required: true }) should detect the absence of a session cookie and always return a non-nullable Session object type.

Detail about proposed feature

If the required option is specified, then an effect should be registered to redirect the user to the sign in page as soon as no session is available.

Potential problems

The session object could not only become nonexistent, but might even change over time. That edge case should be handled separately.

Describe any alternatives you’ve considered

Creating a hook in userland, e.g.:

function useSessionRequired() {
  const [session, loading] = useSession();
  const router = useRouter();

  React.useEffect(() => {
    if (!session && !loading) {
      router.push(`${process.env.NEXTAUTH_URL}/auth/sign-in?error=SessionExpired`);
    }
  }, [loading, router, session]);

  return [session, loading];
}

Additional context

As noticed in #1081, NextAuth.js already listens to page visibility changes. Session emptiness checks should be done each time the page becomes visible after hiding it.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 6
  • Comments: 51 (11 by maintainers)

Most upvoted comments

@kripod So I think I have found a very neat solution/pattern for this, let me know what you think!

// pages/admin.jsx
export default function AdminDashboard () {
  const [session] = useSession() 
  // session is always non-null inside this page, all the way down the React tree.
  return "Some super secret dashboard"
}

AdminDashboard.auth = true
//pages/_app.jsx
export default function App({ Component, pageProps }) {
  return (
    <SessionProvider session={pageProps.session}>
      {Component.auth
        ? <Auth><Component {...pageProps} /></Auth>
        : <Component {...pageProps} />
      }
    </SessionProvider>
  )
}

function Auth({ children }) {
  const [session, loading] = useSession()
  const isUser = !!session?.user
  React.useEffect(() => {
    if (loading) return // Do nothing while loading
    if (!isUser) signIn() // If not authenticated, force log in
  }, [isUser, loading])

  if (isUser) {
    return children
  }
  
  // Session is being fetched, or no user.
  // If no user, useEffect() will redirect.
  return <div>Loading...</div>
}

It can be easily be extended/modified to support something like an options object for role based authentication on pages. An example:

// pages/admin.jsx
AdminDashboard.auth = {
  role: "admin",
  loading: <AdminLoadingSkeleton/>,
  unauthorized: "/login-with-different-user" // redirect to this url
}

Because of how _app is done, it won’t unnecessarily contant the /api/auth/session endpoint for pages that do not require auth.

@kripod Ok you’ve got me, needed to try this out before going to bed ^^

Here’s my TypeScript variant, with added types here and there:

In auth.utils.ts:

/**
 * Authentication configuration
 */
export interface AuthEnabledComponentConfig {
  authenticationEnabled: boolean;
}


/**
 * A component with authentication configuration
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentWithAuth<PropsType = any> = React.FC<PropsType> &
  AuthEnabledComponentConfig;

In _app.tsx:

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types
type NextComponentWithAuth = NextComponentType<NextPageContext, any, {}> &
  Partial<AuthEnabledComponentConfig>;

In whatever page:

import React from 'react';

type FooProps = { }

const FooPage: ComponentWithAuth<FooProps> = (props: FooProps) => {
   ...
  return <foo />;
}

FooPage.authenticationEnabled = true;

export default FooPage;

This seems to be working and feels relatively inoffensive/safe. The _app.tsx file uses Partial just to err on the safe side. No hard contract there, but at least it provides some structure.

As I am too lazy to add .auth on every protected page, I did it by pathname.

Auth component is the same. requireAuth is pretty much the thing I added.

const RESTRICTED_PATHS = ["/restricted"]
...
...
const requireAuth = RESTRICTED_PATHS.some((path) => router.pathname.startsWith(path))

This way, under /restricted, any pages will require a sign-in.

import { Provider, signIn, useSession } from "next-auth/client"
import { AppProps } from "next/app"
import React from "react"

interface Props {}
const Auth: React.FC<Props> = ({ children }) => {
  const [session, loading] = useSession()
  const isUser = !!session?.user
  React.useEffect(() => {
    if (loading) return // Do nothing while loading
    if (!isUser) signIn() // If not authenticated, force log in
  }, [isUser, loading])

  if (isUser) {
    return <>{children}</>
  }

  // Session is being fetched, or no user.
  // If no user, useEffect() will redirect.
  return <div>Loading...</div>
}

const RESTRICTED_PATHS = ["/restricted"]
const MyApp: React.FC<AppProps> = ({ Component, pageProps, router: { route } }) => {
  const requireAuth = RESTRICTED_PATHS.some((path) => route.startsWith(path))

  return (
    <Provider session={pageProps.session}>
      {requireAuth ? (
        <Auth>
          <Component {...pageProps} />
        </Auth>
      ) : (
        <Component {...pageProps} />
      )}
    </Provider>
  )
}

export default MyApp

Hi Guys,

Thank you @balazsorban44 for this pretty solution.

In case someone would like to have a server side redirect solution (eg. 307 HTTP Response) and session obtained on server side - here’s mine solution (may require further improvements):

export const withAuthenticatedOrRedirect = async (context: NextPageContext, destination: string = '/', fn?: (context: NextPageContext) => object) => {
    const session = await getSession(context);
    const isUser = !!session?.user;
    
    // No authenticated session
    if(!isUser) {
        return {
            redirect: {
                permanent: false,
                destination
            }
        };
    }

    // Returned by default, when `fn` is undefined
    const defaultResponse = { props: { session } };

    return fn ? { ...defaultResponse, ...fn(context) } : defaultResponse;
}

Destination is custom user given, but can be modified to automatically go to sign in & redirect back url.

Usage with only auth protection:

export const getServerSideProps = (context: NextPageContext) => withAuthenticatedOrRedirect(context, '/auth/login')

Usage with additional user given serverSideProps:

export const getServerSideProps = (context: NextPageContext) => withAuthenticatedOrRedirect(context, '/auth/login', (context: NextPageContext) => {
  return {
    props: {}
  }
})

Best Regards

@kripod Ok you’ve got me, needed to try this out before going to bed ^^ Here’s my TypeScript variant, with added types here and there: In auth.utils.ts:

/**
 * Authentication configuration
 */
export interface AuthEnabledComponentConfig {
  authenticationEnabled: boolean;
}


/**
 * A component with authentication configuration
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentWithAuth<PropsType = any> = React.FC<PropsType> &
  AuthEnabledComponentConfig;

In _app.tsx:

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types
type NextComponentWithAuth = NextComponentType<NextPageContext, any, {}> &
  Partial<AuthEnabledComponentConfig>;

In whatever page:

import React from 'react';

type FooProps = { }

const FooPage: ComponentWithAuth<FooProps> = (props: FooProps) => {
   ...
  return <foo />;
}

FooPage.authenticationEnabled = true;

export default FooPage;

This seems to be working and feels relatively inoffensive/safe. The _app.tsx file uses Partial just to err on the safe side. No hard contract there, but at least it provides some structure.

I’m a bit inexperienced with TS. Where exactly is NextComponentWithAuth being used? It’s in the _app.tsx file but I don’t know what to do with it. Using Component.auth gives an error that 'auth' doesn’t exist.

function MyApp({ Component, pageProps }: AppProps) {
    ...
}

@abdiweyrah You can replace this

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types
type NextComponentWithAuth = NextComponentType<NextPageContext, any, {}> &
  Partial<AuthEnabledComponentConfig>;

for this

type AppAuthProps = AppProps & {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Component: NextComponentType<NextPageContext, any, {}> & Partial<AuthEnabledComponentConfig>;
};

function MyApp({ Component, pageProps }: AppAuthProps) {
    ...
}

This is exactly what I’ve been looking for! Brilliant stuff. Good job @balazsorban44 ! Really helpful for those of us (me) new to NextJS let alone NextAuth 😃

@kripod So I think I have found a very neat solution/pattern for this, let me know what you think!

// pages/admin.jsx
export default function AdminDashboard () {
  const [session] = useSession() 
  // session is always non-null inside this page, all the way down the React tree.
  return "Some super secret dashboard"
}

AdminDashboard.auth = true
//pages/_app.jsx
export default function App({ Component, pageProps }) {
  return (
    <SessionProvider session={pageProps.session}>
      {Component.auth
        ? <Auth><Component {...pageProps} /></Auth>
        : <Component {...pageProps} />
      }
    </SessionProvider>
  )
}

function Auth({ children }) {
  const [session, loading] = useSession()
  const isUser = !!session?.user
  React.useEffect(() => {
    if (loading) return // Do nothing while loading
    if (!isUser) signIn() // If not authenticated, force log in
  }, [isUser, loading])

  if (isUser) {
    return children
  }
  
  // Session is being fetched, or no user.
  // If no user, useEffect() will redirect.
  return <div>Loading...</div>
}

It can be easily be extended/modified to support something like an options object. An example:

// pages/admin.jsx
AdminDashboard.auth = {
  role: "admin",
  loading: <AdminLoadingSkeleton/>,
  unauthorized: "/login-with-different-user" // redirect to this url
}

Because of how _app is done, it won’t unnecessarily contant the /api/auth/session endpoint for pages that do not require auth.

Just beautiful approach ❤️ nice work!

@abdiweyrah I think you can extend your AppProps like this

export type ProtectedAppProps = AppProps & { Component: NextComponentWithAuth }

I know that has been closed for a while but I think there is always something to contribute. I did some improvements, especially in the TypeScript typings.

Basically, I’ve created the below type:

export type WithAuthentication<P = unknown> = P & {
  requiresAuthentication?: true
}

That allows me to use it on any Next.JS page like this:

const HomePage: WithAuthentication<NextPage> = () => {
  ...
}

HomePage.requiresAuthentication = true

export default HomePage

And on _app like this:

/**
 * Needed to infer requiresAuthentication as a prop of Component
 */
type ComponentWithAuthentication<P> = P & {
  Component: WithAuthentication
}

const MyApp: AppType<{ session: Session | null }> = props => {
  const {
    Component,
    pageProps: { session, ...pageProps },
  } = props as ComponentWithAuthentication<typeof props>

  const OptionalAuthGuard = Component.requiresAuthentication
    ? AuthGuard
    : Fragment

  return (
    <SessionProvider session={session}>
      <OptionalAuthGuard>
        <Component {...pageProps} />
      </OptionalAuthGuard>
    </SessionProvider>
  )
}

I know this is closed, sorry for the necropost, but since the docs reference this issue as an explanation I was reading through anyway. I saw the excellent react-query implementation and wanted to share a similar one I threw together using the awesome (somewhat similar) swr library. I’d be open to publishing this at some point if there’s any interest. Also, please let me know if you spot a bug, missed edge case, or anything 😄

import useSWR from 'swr';
import { useEffect } from 'react';
import { useRouter } from 'next/router';

const isEmpty = (obj) =>
  obj && Object.keys(obj).length === 0 && obj.constructor === Object;

export const useSession = ({
  required = true,
  redirectTo = '/api/auth/signin?error=SessionExpired',
}) => {
  const router = useRouter();
  const { data, error, isValidating, ...rest } = useSWR('/api/auth/session');
  const session = isEmpty(data) ? null : data;

  useEffect(() => {
    if (isValidating) return;
    if (error || (!session && required)) router.push(redirectTo);
  }, [error, isValidating, session, redirectTo, required, router]);

  return { session, error, isValidating, ...rest };
};

Note: the isEmpty stuff is because the default fetcher that swr uses returns truthy {} and I prefer the !session ergonomic.

A nice summary of my approach from https://github.com/nextauthjs/next-auth/issues/1210#issuecomment-782630909 in an article form can be found here: https://simplernerd.com/next-auth-global-session

@kripod I still think your idea is valid, and I am working on a useSession({ required: true }) API change over at #2236.

From now on, let us keep the discussion related to the OPs problem, as my suggestion is a workaround for this issue, rather than an actual solution. 😅

@kripod Ok you’ve got me, needed to try this out before going to bed ^^

Here’s my TypeScript variant, with added types here and there:

In auth.utils.ts:

/**
 * Authentication configuration
 */
export interface AuthEnabledComponentConfig {
  authenticationEnabled: boolean;
}


/**
 * A component with authentication configuration
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentWithAuth<PropsType = any> = React.FC<PropsType> &
  AuthEnabledComponentConfig;

In _app.tsx:

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types
type NextComponentWithAuth = NextComponentType<NextPageContext, any, {}> &
  Partial<AuthEnabledComponentConfig>;

In whatever page:

import React from 'react';

type FooProps = { }

const FooPage: ComponentWithAuth<FooProps> = (props: FooProps) => {
   ...
  return <foo />;
}

FooPage.authenticationEnabled = true;

export default FooPage;

This seems to be working and feels relatively inoffensive/safe. The _app.tsx file uses Partial just to err on the safe side. No hard contract there, but at least it provides some structure.

it works for me …thank you!!

@kripod Ok you’ve got me, needed to try this out before going to bed ^^

Here’s my TypeScript variant, with added types here and there:

In auth.utils.ts:

/**
 * Authentication configuration
 */
export interface AuthEnabledComponentConfig {
  authenticationEnabled: boolean;
}


/**
 * A component with authentication configuration
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentWithAuth<PropsType = any> = React.FC<PropsType> &
  AuthEnabledComponentConfig;

In _app.tsx:

// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/ban-types
type NextComponentWithAuth = NextComponentType<NextPageContext, any, {}> &
  Partial<AuthEnabledComponentConfig>;

In whatever page:

import React from 'react';

type FooProps = { }

const FooPage: ComponentWithAuth<FooProps> = (props: FooProps) => {
   ...
  return <foo />;
}

FooPage.authenticationEnabled = true;

export default FooPage;

This seems to be working and feels relatively inoffensive/safe. The _app.tsx file uses Partial just to err on the safe side. No hard contract there, but at least it provides some structure.

I’m a bit inexperienced with TS. Where exactly is NextComponentWithAuth being used? It’s in the _app.tsx file but I don’t know what to do with it. Using Component.auth gives an error that 'auth' doesn’t exist.

function MyApp({ Component, pageProps }: AppProps) {
    ...
}

In my case i’'m using useEffect => useRouter & getInitialProps => res.writeHead to redirect user to login page. So finally now, i can prevent user from opening Authenticated page both server side & client side. Thank’s @balazsorban44 👍

Folks using react-query, check out https://github.com/nextauthjs/react-query

My solution:

  1. Get session data const { status, data: session } = useSession({ required: true })
  2. Create a conditional to redirect with a component
  3. Add prop requiredRole to Auth component
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      {Component.auth ? (
        <Auth requiredRole={Component.auth.role}> // Step 3
              <Component {...pageProps} />
        </Auth>
      ) : (
            <Component {...pageProps} />
      )}
    </SessionProvider>
  )
}

function Auth({ children, requiredRole }) {
  const { status, data: session } = useSession({ required: true }) // Step 1

  if (status === 'loading') {
    return <div>Loading...</div>
  }

  // Assuming the role is stored in session.user.role
  if (session.user.role !== requiredRole) {
    return <Redirect to="/auth/login" /> // Step 2
  }

  return children
}

// Step 2
function Redirect({ to }) {
  const router = useRouter()

  useEffect(() => {
    router.push(to)
  }, [to, router])

  return null
}
  • NextJS 13.5.3
  • NextAuth 4.23.2

@abdiweyrah I think you can extend your AppProps like this

export type ProtectedAppProps = AppProps & { Component: NextComponentWithAuth }

aand remember renaming Component.auth to Component.authenticationEnabled

if you don’t pass the session from somewhere, then we have to fetch it asynchronously, and thus as you say there will be a brief moment with undefined session. not sure what else to say. we cannot make the session appear without actually fetching it first from somewhere. using getServerSideProps shifts this from blocking part of the UI to blocking as a whole. that’s the compromise.

it’s basically up to you to decide. if you don’t pass it there will be a flash of content when you don’t have an active session. you can mitigate it by for example adding a nice loading skeleton, like the Vercel dashboard. If you think it’s important that your user doesn’t see a screen like that and you are fine with increased TTFB, go with getServerSideProps

@zeing doesn’t have to be in the _app component’s body, you could extract it to its own component. besides, useRouter should work in _app anyway.

@wachidmudi glad it works for you! 🙂

it’s just a convention I had in our app. You’ll potentially end up with many Providers, so I just renamed it to better align with its purpose.