nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
22.87k stars 3.08k forks source link

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

Closed kripod closed 2 years ago

kripod commented 3 years ago

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.

balazsorban44 commented 3 years ago

@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 commented 3 years ago

Whoa, looks like a well-thought pattern with a lot of convenience features included!

I like that. My only concern is about type safety for:

osmarpb97 commented 3 years ago

@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 <3 nice work!

dsebastien commented 3 years ago

@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.

dr-starchildren commented 3 years ago

@balazsorban44 I see SessionProvider here, but the docs say import { Provider } from 'next-auth/client' for using it in _app. Is this a forthcoming change or are the docs out of alignment?


//pages/_app.jsx
export default function App({ Component, pageProps }) {
  return (
    <SessionProvider session={pageProps.session}>
      {Component.auth
        ? <Auth><Component {...pageProps} /></Auth>
        : <Component {...pageProps} />
      }
    </SessionProvider>
  )
}
balazsorban44 commented 3 years ago

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.

osmarpb97 commented 3 years ago

I got a little bug with this approach I got redirected when I put some query in the URL like localhost:3000/?id=1 or in dynamic routes when I refresh the page, it's just me? @balazsorban44 I tested with 2 different apps and the same thing happen :(

balazsorban44 commented 3 years ago

Hard to tell. It's not really a feature of next-auth, but something I implemented in user land. Therefore I don't think I can help resolve it here. Could you open a discussion, and add a reproduction?

j-lee8 commented 3 years ago

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 :)

balazsorban44 commented 3 years ago

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

zeing commented 3 years ago

in this example how to use unauthorized in _app.js

balazsorban44 commented 3 years ago

@zeing if you refer to my suggestion, you will need to extend the logic here:

{Component.auth
        ? <Auth><Component {...pageProps} /></Auth>
        : <Component {...pageProps} />
      }

As Compnent.auth will not be a simple boolean anymore, but an object.

zeing commented 3 years ago

@zeing if you refer to my suggestion, you will need to extend the logic here:


{Component.auth

        ? <Auth><Component {...pageProps} /></Auth>

        : <Component {...pageProps} />

      }

As Compnent.auth will not be a simple boolean anymore, but an object.

I try to think about How to redirect or use useRouter in _appjs ?

wachidmudi commented 3 years ago

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 👍

balazsorban44 commented 3 years ago

@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! 🙂

fwyh commented 3 years ago

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

github0013 commented 2 years ago

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
balazsorban44 commented 2 years ago

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. 😅

balazsorban44 commented 2 years ago

This is now available in 4.0.0-next.18!

ghost commented 2 years ago

@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) {
    ...
}
mkalvas commented 2 years ago

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.

orangecoloured commented 2 years ago

@abdiweyrah I think you can extend your AppProps like this

export type ProtectedAppProps = AppProps & { Component: NextComponentWithAuth }

trulymittal commented 2 years ago

@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.

Do we still need to pass the session in the pageProps like so from every page, since SessionProvider docs says so?

export async function getServerSideProps(ctx) {
  return {
    props: {
      session: await getSession(ctx)
    }
  }
}

If NOT passing session as page prop then how will the SessionProvider will have session, because it will always be undefined.

balazsorban44 commented 2 years ago

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

trulymittal commented 2 years ago

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

Here I am not worried about the TTFB, but instead, since we are extracting the session from the pageProps and if we do not use the getServerSideProps() to pass session as prop, then no session would be passed to pageProps, and thus the session from pageProps will always be undefined, so what is the use of then passing the session to SessionProvider - if we are always passing it as undefined, OR I am missing something here...

balazsorban44 commented 2 years ago

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.

adamduncan commented 2 years ago

@balazsorban44: 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

Perhaps another important consideration here is what data are required for the page and fetched on the server? E.g. An authenticated route might be importing/fetching data and passing along to the page props in getServerSideProps(). That data should inherently be protected. Without checking the session on the server, we won't have the mechanism to determine if a user should have access to that page's data.

With the existing approach, one can cURL the URL and will have access to that protected data in the server response's __NEXT_DATA__'s pageProps that are injected onto the page.

One approach we considered to mitigate this was to conditionally pass empty props back from the server:

export async function getServerSideProps(context) {
  // Do some data fetching for protected content

  return {
    props: (await getSession(context)) ? { content } : {},
  };
}

Though I expect this might detract from the benefits outlined in this Client Session Handling solution? In which case, do we also lose the ability to manage this pre-session state gracefully?

Edit: In which case might it just make more sense to do a getServerSideProps redirect instead?

EzeGmnz commented 2 years ago

@abdiweyrah I think you can extend your AppProps like this

export type ProtectedAppProps = AppProps & { Component: NextComponentWithAuth }

aand remember renaming Component.auth to Component.authenticationEnabled

medarma86 commented 2 years ago

@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.

@balazsorban44 I am trying to implement the solution above which you have shared in the following : https://simplernerd.com/next-auth-global-session

but in the app.tsx am getting error for component.auth prop as 'Property 'auth' does not exist on type 'NextComponentType<NextPageContext, any, Record<string, unknown>>'. Property 'auth' does not exist on type 'ComponentClass<Record<string, unknown>, any> & { getInitialProps?(context: NextPageContext): any; }'.ts(2339)'

how to make the auth property available in nextpagecontext properties??

medarma86 commented 2 years ago

@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!!

willemmulder commented 2 years ago

@balazsorban44 Just to make sure I understand your solution correctly (coming from https://next-auth.js.org/getting-started/client#custom-client-session-handling) : it uses _app.js to wrap .auth-flagged components with an Auth component that will do the session-fetching (and revalidation) for us.

Thus, it is a higher-level alternative to showing a loading-state in every individual component, right?

How should I interpret the text on https://next-auth.js.org/getting-started/client#custom-client-session-handling that "every page transition afterward will be client-side"? If I refresh a page, the session will still be-refetched, right?

"Due to the way Next.js handles getServerSideProps / getInitialProps, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages."

Thanks anyways! 👍

gbrlmrllo commented 2 years ago

@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) {
    ...
}
mark-voltacharging commented 2 years ago

This pattern seems to give me an infinite loop of redirection if I have a custom sign-in page, has anyone else experienced this?

pages: { signIn: '/auth/sign-in' }

jayzehngebot commented 2 years ago

This pattern seems to give me an infinite loop of redirection if I have a custom sign-in page, has anyone else experienced this?

pages: { signIn: '/auth/sign-in' }

I think i'm in the same boat, here

WilderDev commented 1 year ago

Getting a typescript error: "Property 'auth' does not exist on type 'FunctionComponent<{}> & { getInitialProps?(context: NextPageContext): {} | Promise<{}>; }'.ts(2339)"

Basically I can't do: MyComponent.Auth because MyComponent: NextPage = () => {...} and NextPage doesn't have auth property... How do I add that again?

jaloveeye commented 1 year ago

Getting a typescript error: "Property 'auth' does not exist on type 'FunctionComponent<{}> & { getInitialProps?(context: NextPageContext): {} | Promise<{}>; }'.ts(2339)"

Basically I can't do: MyComponent.Auth because MyComponent: NextPage = () => {...} and NextPage doesn't have auth property... How do I add that again?

@WilderDev Try this.

type NextPageWithAuth<P = {}, IP = P> = NextPage<P, IP> & { auth?: boolean }

const MyComponent: NextPageWithAuth = () => { ... }
centralcybersecurity commented 1 year ago

Hi, getting the below error in typescript.. can anyone help?

info - Linting and checking validity of types .Failed to compile. ./pages/_app.tsx:12:32 Type error: Cannot find name 'NextPageContext'.

import { NextComponentType } from "next"; import { AuthEnabledComponentConfig } from "../auth.utils";

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

orangecoloured commented 1 year ago

@newlaravelcoder seems like you need to import NextPageContext to use it.

centralcybersecurity commented 1 year ago

Thanks, got it. But still am getting the below error:

Type error: Property 'auth' does not exist on type 'NextComponentType<NextPageContext,

function App({ Component, pageProps }: AppAuthProps) { return (
console.log(pageProps.session),

{Component.auth ? ( ) : ( )}

); };

centralcybersecurity commented 1 year ago

Type '{ session: Session; }' has no properties in common with type 'IntrinsicAttributes & { children?: ReactNode; }'.

Edited: Fixed - removing these lines: AppProps<{ session: Session; }> with AppProps

==== FULL CODE (Fixed):

import { Session } from "next-auth";
import { useSession, SessionProvider, signIn } from 'next-auth/react';
import { AppProps } from "next/app";
import "styles/custom.css";
import { NextComponentType, NextPageContext } from "next";
import { AuthEnabledComponentConfig } from "../auth.utils"; 

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

function App({ Component, pageProps }: AppAuthProps) {
  return (    
    console.log(pageProps.session),
    <SessionProvider session={pageProps.session}>
      {Component.authenticationEnabled ? (
        <Auth>
          <Component {...pageProps} />
        </Auth>
      ) : (
        <Component {...pageProps} />
      )}
  </SessionProvider>
  );
};

function Auth({ children }) {
  // if `{ required: true }` is supplied, `status` can only be "loading" or "authenticated"
  const { data: session, status } = useSession({ required: true })
  const isUser = !!session?.user 

  React.useEffect(() => {
      if (status === 'loading') return   // Do nothing while loading
      if (!isUser) signIn()
  }, [isUser, status])

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

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

  return <div>Loading...</div>

}

export default App;
emiliosheinz commented 1 year ago

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>
  )
}
centralcybersecurity commented 1 year ago

Thank you, let me try it out. btw, do you also know how to redirect users based on role using GitHub oath - as soon as redirect, the default user role should be set in the database as 'user' role. Preferably in the middleware (using NextJS 13 & Next-Auth)

I have 4 users and 4 separate dashboard. /adminDashboard - role:admin /internDashboard - role:intern /editorDashboard - role:editor /public - role:user (default)

Thank you again.

centralcybersecurity commented 1 year ago

authenticationEnabled

getting this error: ReferenceError: AuthGuard is not defined

Also could you explain how this approach is better than the above typescript approach?

Thanks

emiliosheinz commented 1 year ago

authenticationEnabled

getting this error: ReferenceError: AuthGuard is not defined

Hei @newlaravelcoder, about your error it seems that you have not imported the AuthGuard Component. Looking at your code it seems that it is the function Auth({ children }) { component that you created in _app. I would say you only need to change the code where I verify if the component has required authentication (Component.requiresAuthentication). In your case it will be something like:

  const OptionalAuthGuard = Component.requiresAuthentication
    ? AuthGuard
    : Fragment

Also could you explain how this approach is better than the above typescript approach?

I like this solution better because I understand that with fewer and reusable types, in the long term, the codebase would be easier to understand. Using typeof props we are taking the type that was inferred and simply extending it.

Best!

julortiz commented 8 months ago

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
}
emiliosheinz commented 8 months ago

Sounds like a great solution!!

AndonMitev commented 7 months ago

Guys, please help can't understand how to read the token in order to execute api call:

const getAccessToken = async (req: NextApiRequest) => {
  const session = (await getSession({ req })) as any;
  return session?.accessToken;
};

export const updatePasswordApi = async (data: {
  oldPassword: string;
  newPassword: string;
}) => {
  const accessToken = await getAccessToken() - what and how to pass (req: NextApiRequest);

  const response = await fetch(baseUrl, {
    method: 'POST',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer {accessToken}`
    }
  });

  if (!response.ok) {
    throw new Error(API_ERRORS.UPDATE_PASSWORD);
  }

  return response.json();
};

i have tried like everything and can't come up with solution

emiliosheinz commented 7 months ago

Not sure if I understand your question, but It looks like in the code you provided you are saving the access token as accessToken and trying to use it as jwt

AndonMitev commented 7 months ago

@emiliosheinz, sorry just updated, my question is how to extract the access token in services that will be used to make api calls, feel like i'm missing some piece but cant see which one exactly

emiliosheinz commented 7 months ago

Since this may vary based on things such as where you are calling this function, It's hard to say without the full picture of your project.