0

I'm building a Next.js application using next-auth with the AzureAD provider. My users sign in using their Microsoft accounts, and the authentication generally works well.

However, I’ve run into a problem when users have multiple Microsoft accounts signed in in their browser (e.g. via https://login.microsoftonline.com or Office 365). When this happens, my silent sign-in using signIn("azure-ad", { prompt: "none" }) no longer works reliably.

In some cases, it prompts the user to select an account, which breaks the flow — especially when used inside an iframe or during background token refresh. It appears that other Microsoft accounts currently active in the browser session are interfering with the session and the access token that NextAuth tries to use.

Here’s a simplified version of my setup:

I use signIn("azure-ad", { prompt: "none", redirect: false }) every 60 minutes to silently refresh the session.

This works fine as long as the user only has one Microsoft account signed in.

Once multiple accounts are active, the user is prompted to choose an account, even with prompt: "none", and this breaks the silent login flow.

My question is:

How can I ensure that only the Microsoft account the user originally signed in with is used for silent re-authentication in NextAuth, and that other active accounts in the browser do not interfere?

Bonus points:

If there's a way to force AzureAD to always pick the originally signed-in account, or use a session-specific hint (like login_hint or domain_hint) in combination with prompt: "none", I'd love to know the correct usage pattern.

Any advice or best practices on handling multi-account scenarios with Azure AD and NextAuth are welcome!

my current nextauth:

import NextAuth from "next-auth";
import AzureADProvider from "next-auth/providers/azure-ad";
import axios from "axios";

const refreshAccessToken = async (refreshToken) => {
  const url = `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}/oauth2/v2.0/token`;

  const details = {
    client_id: process.env.AZURE_AD_CLIENT_ID,
    client_secret: process.env.AZURE_AD_CLIENT_SECRET,
    grant_type: "refresh_token",
    refresh_token: refreshToken,
    scope: "openid profile email offline_access User.Read GroupMember.Read.All",
  };

  const formBody = new URLSearchParams(details).toString();

  try {
    const response = await axios.post(url, formBody, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
    });

    return response.data;
  } catch (error) {
    if (error.response) {
      console.error("❌ Token refresh error response:", error.response.data);
    } else {
      console.error("❌ Token refresh unknown error:", error.message);
    }
    throw new Error("Token refresh failed");
  }
};

const authOptions = {
  providers: [
    AzureADProvider({
      clientId: process.env.AZURE_AD_CLIENT_ID,
      clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
      tenantId: process.env.AZURE_AD_TENANT_ID,
      authorizationUrl: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}/oauth2/v2.0/authorize`,
      tokenUrl: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_AZURE_AD_TENANT_ID}/oauth2/v2.0/token`,
      profileUrl: "https://graph.microsoft.com/v1.0/me",
      scope:
        "openid profile email offline_access User.Read GroupMember.Read.All",
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      // Eerste login: access & refresh token opslaan
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.accessTokenExpires = Date.now() + account.expires_in * 1000;
        return token;
      }

      // Als het access token nog geldig is, geef het terug
      if (Date.now() < token.accessTokenExpires) {
        return token;
      }

      // Token is verlopen, probeer te vernieuwen
      try {
        console.log("🔁 Access token verlopen, vernieuwen...");
        const refreshedTokens = await refreshAccessToken(token.refreshToken);

        return {
          ...token,
          accessToken: refreshedTokens.access_token,
          accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
          refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
        };
      } catch (error) {
        console.error("❌ Token refresh mislukt:", error);
        return {
          ...token,
          error: "RefreshAccessTokenError",
        };
      }
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.error = token.error; // optioneel
      return session;
    },
    async signIn({ account }) {
      const accessToken = account.access_token;
      if (!accessToken) {
        console.error("No access token found");
        return false;
      }

      try {
        const response = await fetch(
          `https://graph.microsoft.com/v1.0/me/memberOf`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        if (!response.ok) {
          console.error("Error fetching groups:", response.statusText);
          return false;
        }

        const groupData = await response.json();

        if (!groupData || !groupData.value) {
          console.error("Invalid group data:", groupData);
          return false;
        }

        const groupIds = groupData.value.map((group) => group.id);
        const allowedGroupIds = ["xxx-xxx-xxx"]; //test

        const isAllowed = groupIds.some((groupId) =>
          allowedGroupIds.includes(groupId)
        );

        if (isAllowed) {
          return true;
        } else {
          return false;
        }
      } catch (error) {
        console.error("Error during signIn callback:", error);
        return false;
      }
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

authprovider:

"use client";
import { createContext, useEffect, useState, useContext } from "react";
import { useSession, getSession, signIn } from "next-auth/react"; // ⬅️ signIn toegevoegd

export const AuthContext = createContext(null);

function AuthProvider({ children }) {
  const { data: session, status } = useSession();
  const [groupIds, setGroupIds] = useState([]);

  const fetchGroupData = async (accessToken) => {
    console.log("📥 fetchGroupData aangeroepen met token:", accessToken);

    try {
      const response = await fetch(
        "https://graph.microsoft.com/v1.0/me/memberOf",
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );
      const groupData = await response.json();
      if (groupData?.value) {
        console.log("✅ groupData ontvangen:", groupData.value);
        setGroupIds(groupData.value.map((group) => group.id));
        console.log("✅ groupid opgeslagen");
      }
    } catch (error) {
      console.error("❌ Fout bij ophalen groepen:", error);
    }
  };

  useEffect(() => {
    if (session?.accessToken) {
      fetchGroupData(session.accessToken);
    }
  }, [session?.accessToken]);

  useEffect(() => {
    console.log("✅ Interval gestart");
    // const userId = session.user.email;

    const interval = setInterval(async () => {
      console.log("🔄 Vernieuw sessie (silent)...");
      await signIn("azure-ad", {
        prompt: "none",
        // loginHint: userId,
        redirect: false,
      });

      const refreshedSession = await getSession();
      console.log("✅ Vernieuwde session:", refreshedSession);

      if (refreshedSession?.accessToken) {
        fetchGroupData(refreshedSession.accessToken);
      }
    }, 60 * 1000); // elke 60 minuten

    return () => {
      console.log("🛑 Interval gestopt");
      clearInterval(interval);
    };
  }, []);

  return (
    <AuthContext.Provider value={{ session, groupIds }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

export default AuthProvider;

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.