Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 581
sabarinath Avatar asked Sep 01 '25 15:09

sabarinath


2 Answers

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.

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.

like image 74
Seeker Avatar answered Sep 05 '25 06:09

Seeker


I tried the following React 18 with Next.js 14 code using AppRouter for Single Sign On.

Code :

src/components/ProtectedRoute.tsx :

import { useRouter } from 'next/router';
import { useMsal } from '@azure/msal-react';
import React from 'react';

const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    const router = useRouter();
    const { instance } = useMsal();
    const isAuthenticated =  true;
    if (!isAuthenticated) {
        router.push('/login');
        return null;
    }
    return <>{children}</>;
};
export default ProtectedRoute;

src/layouts/Layout.tsx :

import React, { ReactNode } from 'react';
import { MsalProvider } from '@azure/msal-react';
import { PublicClientApplication } from '@azure/msal-browser'; 
import { msalConfig } from '../msalConfig';

interface LayoutProps {
    children: ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
    const msalInstance = new PublicClientApplication(msalConfig);
    return (
        <MsalProvider instance={msalInstance}>
            <div>
                {children}
            </div>
        </MsalProvider>
    );
};
export default Layout;

src/pages/login.tsx :

import React from 'react';
import { useMsal } from '@azure/msal-react';

const Login: React.FC = () => {
    const { instance } = useMsal();
    const handleLogin = () => {
        instance.loginPopup();
    };
    return (
        <div>
            <h1>Login Page</h1>
            <button onClick={handleLogin}>Login with Microsoft</button>
        </div>
    );
};
export default Login;

src/pages/index.tsx :

import React from 'react';
import { useMsal } from '@azure/msal-react';

const Home: React.FC = () => {
    const { instance, accounts } = useMsal();
    const handleSignIn = () => {
        instance.loginPopup();
    };
    const handleSignOut = () => {
        instance.logout();
    };
    return (
        <div>
            {accounts.length === 0 && (
                <>
                    <h1>Welcome to my Next.js App</h1>
                    <button onClick={handleSignIn}>Sign In</button>
                </>
            )}
            {accounts.length > 0 && (
                <button onClick={handleSignOut}>Sign Out</button>
            )}
        </div>
    );
};
export default Home;

src/pages/_app.tsx :

import React from 'react';
import { AppProps } from 'next/app';
import Layout from '../layouts/Layout';

const MyApp: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => {
    return (
        <Layout>
            <Component {...pageProps} />
        </Layout>
    );
};
export default MyApp;

src/msalConfig.ts :

export const msalConfig = {
    auth: {
        clientId: '<client_ID>',
        authority: 'https://login.microsoftonline.com/<tenant_ID>',
        redirectUri: 'http://localhost:3000', 
    },
};

I added the output URL in the Azure app's Authentication as a Single-page application, as shown below.

http://localhost:3000/

enter image description here

Output :

The following Next.js code ran successfully, as demonstrated below.

I successfully signed in and signed out by clicking on the Sign In and Sign Out buttons, as below.

enter image description here

enter image description here

enter image description here

like image 37
Dasari Kamali Avatar answered Sep 05 '25 06:09

Dasari Kamali