azure-active-directorynext-authapp-routernext.js14msal-react

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


I am working on a React web application which is using React 18 and Next.js 14. I am using App Router as recommended by Next.js boiler plate set-up which creates app folder inside src folder. Is 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

    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

    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

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

    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.

    Step 6: Setting NEXTAUTH_URL.

    Last but not least. NEXTAUTH_URL is key for next-auth service to capture your redirection url post authentication.

    if not specified., it default redirects to localhost.

    As long as you are working in localhost., NEXTAUTH_URL is not mandatory. But when deploying to environments like dev, qa and prod., it is must to define NEXTAUTH_URL in .env.

    it will be something like this NEXTAUTH_URL=https://myapp.com

    I suppose this removes your blocker.