azurenext.jsazure-active-directorymicrosoft-graph-apiazure-ad-b2c

I am trying to update an existing user i made through graph api in my code, but i get a 403 error back from azure graph api


So i am trying to send a patch request to an existing user to update some info. i already have the api permissions set and granted them consent, so i dont really get why i get this error;

{
    "error": {
        "code": "Authorization_RequestDenied",
        "message": "Insufficient privileges to complete the operation.",
        "innerError": {
            "date": "2024-12-30T06:59:17",
            "request-id": "55f9f873-b7b3-424a-95ab-5b5f21e3593b",
            "client-request-id": "// wont show this"
        }
    }
}

I am a global administrator as u can see here role

also these are my api permissions since i use graph api in my nextjs code. api

and this is my code if needed.

import { useState, useEffect } from "react";
import axios from "axios";
import Head from "next/head";
import { useRouter } from "next/router";

interface FormData {
  displayName: string;
  mailNickname: string;
  password: string;
  customField: string;
  organizationalUnitCode: string;
}

interface OrganizationalUnit {
  code: string;
  name: string;
}

const getAccessToken = async (): Promise<string> => {
  try {
    const response = await axios.post("/api/register");
    return response.data.accessToken;
  } catch (error) {
    console.error("Error fetching token:", error);
    throw new Error("Failed to fetch access token");
  }
};
const findExistingUserByEmail = async (accessToken: string, email: string) => {
  try {
    const response = await axios.get(
      `https://graph.microsoft.com/v1.0/users?$filter=mail eq '${email}'`,
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );

    if (response.data.value.length > 0) {
      const user = response.data.value[0];
      console.log("Found user:", user);  // Log the user to check the id
      return user; // This returns the user object with id and other properties
    }

    return null;
  } catch (error) {
    const err = error as any;
    console.error("Error finding user by email:", err.response?.data || err.message);
    throw new Error("Failed to find user by email");
  }
};



const updateUser = async (accessToken: string, userId: string, userData: FormData) => {
  console.log(userId)
  try {
    const response = await axios.patch(
      `https://graph.microsoft.com/v1.0/users/${userId}`,
      {
        id: userId,
        displayName: userData.displayName,
        mailNickname: userData.mailNickname,
        passwordProfile: userData.password ? { password: userData.password } : undefined,
        [ `extension_${process.env.NEXT_PUBLIC_AZURE_AD_B2C_Extension_ID}_organizationalUnitCode` ]: userData.organizationalUnitCode,
      },
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );

    console.log("User updated successfully:", response.data);
    return response.data;
  } catch (error) {
  const err = error as any;
  console.error("Error updating user:", err.response?.data || err.message);
  throw new Error(`Failed to update user: ${err.response?.data?.error?.message || err.message}`);
}

};


const createUser = async (accessToken: string, userData: FormData) => {
  try {
    const mailNickname = userData.mailNickname;
    const email = userData.displayName;

    if (!email.includes("@")) {
      throw new Error("Display Name must be a valid email address.");
    }

    const domain = email.split("@")[1];
    const formattedMailNickname = `${mailNickname}_${domain.split(".")[0]}.com`;
    const formattedEmail = `${formattedMailNickname}#EXT#@floadingwatkanikladen.onmicrosoft.com`;

    const response = await axios.post(
      "https://graph.microsoft.com/v1.0/users",
      {
        accountEnabled: true,
        displayName: userData.displayName,
        mailNickname: formattedMailNickname,
        userPrincipalName: formattedEmail,
        otherMails: [userData.displayName],
        passwordProfile: {
          forceChangePasswordNextSignIn: false,
          password: userData.password,
        },
        [`extension_${process.env.NEXT_PUBLIC_AZURE_AD_B2C_Extension_ID}_organizationalUnitCode`]:
          userData.organizationalUnitCode,
      },
      {
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      }
    );

    return response.data;
  } catch (err) {
    console.error("Error creating user:", err);
    throw err;
  }
};

const SignupForm = () => {
  const router = useRouter();
  const { email, organizationalUnitCode } = router.query;

  const [formData, setFormData] = useState<FormData>({
    displayName: "",
    mailNickname: "",
    password: "",
    customField: "",
    organizationalUnitCode: "",
  });

  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);
  const [success, setSuccess] = useState<string | null>(null);
  const [organizationalUnits, setOrganizationalUnits] = useState<OrganizationalUnit[]>([]);

  useEffect(() => {
    const fetchOrganizationalUnits = async () => {
      try {
        const response = await axios.get(
          "https://watkanikladenapi2-gwhpc3htfzhrh7e2.westeurope-01.azurewebsites.net/organizational-units"
        );
        setOrganizationalUnits(response.data.organizationalUnits);
      } catch (error) {
        console.error("Error fetching organizational units:", error);
      }
    };

    fetchOrganizationalUnits();
  }, []);

  useEffect(() => {
    if (email) {
      setFormData((prevData) => ({
        ...prevData,
        mailNickname: email as string,
        organizationalUnitCode: organizationalUnitCode as string || "",
      }));
    }
  }, [email, organizationalUnitCode]);

  const handleCreate = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(null);

    try {
      const accessToken = await getAccessToken();
      const result = await createUser(accessToken, formData);
      console.log("User created:", result);
      setSuccess("User created successfully!");
    } catch (err) {
      console.error("Error creating user:", err);
      setError(`An error occurred: ${(err as Error).message}`);
    } finally {
      setLoading(false);
    }
  };

  const handleUpdate = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setSuccess(null);
  
    try {
      const accessToken = await getAccessToken();
      
      // Find the existing user by email
      const existingUser = await findExistingUserByEmail(accessToken, formData.displayName);
  
      if (existingUser) {
        console.log("User ID to update:", existingUser.id); // Check the ID here
        const result = await updateUser(accessToken, existingUser.id, formData);
        console.log("User updated:", result);
        setSuccess("User updated successfully!");
      } else {
        setError("User not found for updating.");
      }
    } catch (err) {
      console.error("Error updating user:", err);
      setError(`An error occurred: ${(err as Error).message}`);
    } finally {
      setLoading(false);
    }
  };
  

  return (
    <>
      <Head>
        <link
          href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
          rel="stylesheet"
        />
      </Head>

      <div className="min-h-screen flex items-center justify-center bg-gray-100 py-12 px-4">
        <div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full">
          <h1 className="text-2xl font-bold text-center text-gray-800 mb-6">Sign Up / Update</h1>
          <form>
            <input
              type="text"
              placeholder="Display Name"
              value={formData.displayName}
              onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
              className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <input
              type="text"
              placeholder="Mail Nickname"
              value={formData.mailNickname}
              onChange={(e) => setFormData({ ...formData, mailNickname: e.target.value })}
              className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
              readOnly={!!email}
            />
            <input
              type="password"
              placeholder="Password"
              value={formData.password}
              onChange={(e) => setFormData({ ...formData, password: e.target.value })}
              className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            />

            <select
              value={formData.organizationalUnitCode}
              onChange={(e) => setFormData({ ...formData, organizationalUnitCode: e.target.value })}
              className="w-full p-3 mb-4 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
              <option value="">Select Organizational Unit</option>
              {organizationalUnits.map((unit) => (
                <option key={unit.code} value={unit.code}>
                  {unit.name}
                </option>
              ))}
            </select>

            <button
              onClick={handleCreate}
              disabled={loading}
              className="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 mb-4"
            >
              {loading ? "Processing..." : "Create User"}
            </button>
            <button
              onClick={handleUpdate}
              disabled={loading}
              className="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            >
              {loading ? "Processing..." : "Update User"}
            </button>

            {error && <p className="text-red-500 text-center mt-4">{error}</p>}
            {success && <p className="text-green-500 text-center mt-4">{success}</p>}
          </form>
        </div>
      </div>
    </>
  );
};

export default SignupForm;

enter code here

Solution

  • The error "Insufficient privileges to complete the operation" usually occurs if the access token does not contain required scope and roles to perform the action.

    I granted API permissions like below:

    enter image description here

    For sample, I generated the access token by passing below parameters:

    https://login.microsoftonline.com/B2CTenantID/oauth2/v2.0/token
    
    grant_type: client_credentials
    client_id: ClientID
    client_secret: Secret
    scope: https://graph.microsoft.com/.default
    

    enter image description here

    Decoded access token:

    enter image description here

    I got the same error when tried to update the user:

    PATCH https://graph.microsoft.com/v1.0/users/{id}
    Content-type: application/json
    
    {
      "displayName": "ruknew",
      "mailNickname": "rukk",
      "passwordProfile": {
        "forceChangePasswordNextSignIn": false,
        "password": "xxx"
      }
    }
    
    

    enter image description here

    Note: For application-only access, the calling application must have the User.ReadWrite.All permission (for least privilege) or the Directory.ReadWrite.All permission (for higher privilege), along with the User Administrator role in Microsoft Entra.

    Hence to resolve the error, you need to assign Microsoft Entra ID application with the User Administrator role:

    Search the app name the app will show up in the Enterprise applications

    enter image description here

    enter image description here

    Regenerate the access token and I am able to successfully update the user details:

    enter image description here

    Reference:

    Update user - Microsoft Graph v1.0 | Microsoft