Search code examples
azure-active-directorynext-authapp-routernext.js14msal-react

How to do azure single sign on with next.js 14 and App Router


Iam working on a React web application which is using React 18 and Next.js 14. Iam using App Router as recommended by Next.js boiler plate set-up which creates app folder inside src folder.. iam using layout.tsx as entry point for my application.

I do not have pages folder or _app.tsx as mentioned in many articles.

I have gone through official documentaion of next.js ( here ). But no luck in finding clear detail about configuring Azure SSO with App Router for my Next.js app.

Just wondering., if any article available which explains the flow in detail. or working example with above prerequsites?


Solution

  • App Router can be little tricky when handling redirection while implementing SSO. I will guide you through step by step to configure next-auth to your React 18, next.js 14 and App Router.

    step 1: Install dependencies

    • Install next-auth using npm i next-auth command

    step 2: Setting up Path for next-auth configuration

    1. Since you are using App Router., You must see app folder inside src
    2. Also., you state you are using Next.js 14., As per official documentation of [next-auth][1]., configuration has been made compatible for App Router as follows
    3. create api folder inside app folder
    4. create auth folder inside api
    5. create [...nextauth] folder inside auth folder
    6. create route.ts file inside [...nextauth] folder
    7. ideally your path should look like this src/app/api/auth/[..nextauth]/router.ts

    step 3: Setting up next-auth configuration

    • Set up your redirection URL as follow
    • https://localhost:3000/api/auth/callback/azure-ad in Azure App registration redirect URL.
    • for production: https://yourapplication.com/api/auth/callback/azure-ad in Azure App registration redirect URL.
    • For more details., refer here for step by step guide
    • Setup your router.ts. Copy below code and customize to your requirements

    import NextAuth from "next-auth";
    import AzureADProvider from "next-auth/providers/azure-ad";
    
    const { AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID } =
      process.env;
    
    if (!AZURE_AD_CLIENT_ID || !AZURE_AD_CLIENT_SECRET || !AZURE_AD_TENANT_ID) {
      throw new Error("The Azure AD environment variables are not set.");
    }
    
    const handler = NextAuth({
      secret: AZURE_AD_CLIENT_SECRET,
      providers: [
        AzureADProvider({
          clientId: AZURE_AD_CLIENT_ID,
          clientSecret: AZURE_AD_CLIENT_SECRET,
          tenantId: AZURE_AD_TENANT_ID,
        }),
      ],
      callbacks: {
        async jwt({ token, account }) {
          if (account) {
            token = Object.assign({}, token, {
              access_token: account.access_token,
            });
          }
          return token;
        },
        async session({ session, token }) {
          if (session) {
            session = Object.assign({}, session, {
              access_token: token.access_token,
            });
            console.log(session);
          }
          return session;
        },
      },
    });
    
    export { handler as GET, handler as POST };

    Note: You can decode access_token and id_token to fetch groups, token_expiry etc with help of jwt-decode subject to your requirements.

    Step 4: Setting up SessionProvider

    • Go to layout.tsx and wrap your app inside SessionProvider as follows

    "use client";
    import React, { useRef } from "react";
    import "./globals.css";
    import { Box } from "@mui/material";
    import { AppStore, makeStore } from "@/lib/store";
    import { Provider } from "react-redux";
    import { PersistGate } from "redux-persist/integration/react";
    import { SessionProvider } from "next-auth/react";
    import { ProtectedComponents } from "./components/ProtectedComponents";
    
    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      const storeRef = useRef<AppStore>();
      if (!storeRef.current) {
        storeRef.current = makeStore();
      }
    
      return (
        <html lang="en">
          <body>
            <SessionProvider>
              <Provider store={storeRef.current}>
                <PersistGate
                  loading={null}
                  persistor={storeRef.current.__persistor}
                >
                  <Box sx={{ display: "flex" }}>
                    <ProtectedComponents>{children}</ProtectedComponents>
                  </Box>
                </PersistGate>
              </Provider>
            </SessionProvider>
          </body>
        </html>
      );
    }

    Note: Do not worry about importing route.js code., next-auth automatically picks configuration if placed in right place

    In above code., I have mentioned ProtectedComponents to access subject to session availability.

    Lets try to set ProtectedComponents by picking session information as follows.

    import React, { useEffect, ReactNode } from "react";
    import { useSession } from "next-auth/react";
    import { usePathname, useRouter } from "next/navigation";
    import { Grid } from "@mui/material";
    import Header from "./Header";
    
    export const ProtectedComponents = ({ children }: { children: ReactNode }) => {
      const { data: session, status, update } = useSession();
      const router = useRouter();
      const pathName = usePathname();
    
      useEffect(() => {
        if (status === "loading") return; // Do nothing while loading
        if (session && pathName === "/") router.push("/dashboard");
        if (!session) router.push("/"); // If not authenticated, force log in
      }, [session, status]);
    
      return (
        <Grid container sx={{ backgroundColor: "#F8F8F9" }}>
          <Grid item xs={true}>
            <Grid container>
              {session && pathName !== "/" && (
                <Grid item xs={12}>
                  <Header />
                </Grid>
              )}
              <Grid item p={2} xs={true}>
                {children}
              </Grid>
            </Grid>
          </Grid>
        </Grid>
      );
    };

    Step 5: Setting up login and logout methods.

    • Login: Inbuilt methods are prodided for signIn and signOut by next-auth as follows

    for login:

    import { signIn } from "next-auth/react";
    const handleLoginClick = async () => {
      try {
        signIn();
      } catch (error) {
        console.error(error);
      }
    };
    <Button
      variant="contained"
      color="primary"
      fullWidth
      onClick={handleLoginClick}
    >
      Login
    </Button>;

    For logout:

    import { signOut } from "next-auth/react";
    const handleLogOutClick = async () => {
      try {
        signOut();
      } catch (error) {
        console.error(error);
      }
    };
    <Button
      variant="contained"
      color="primary"
      fullWidth
      onClick={handleLogOutClick}
    >
      Login
    </Button>;

    This basically concludes the initial setup required for next-auth configuration to do Azure single sign on.

    I suppose this removes your blocker.