Loading...
Azure-Ad-B2C-Rest-Part2
Author Cloudapp
E.G.

Azure AD B2C/Prisma ORM - Data control via REST - Part 2

June 4, 2024
Table of Contents

In my first story (Manipulate Azure AD B2C Data), I explained the basic setup of Prisma for PostgreSQL (I used the free plan from Neon.tech) and how you can update your User Objects within the Azure AD Object Store. We will examine how you can further manipulate your data in both sources.

Here is the GitHub repo with the full code.

In my previous story, regarding CRUD operations on Azure AD B2C, I explained how you can “Patch” (modify) data attributes of your User Objects and how you can create a new user record with the famous ORM Prisma in your PostgreSQL DB (I used the free offering of neon.tech).

Below is the Next.js code snippet for storing a new user in the “roles” table with Prisma. We pass only one attribute, “name”.

data = await prisma.roles.create({
      data: {
        name: name,
      },
    });

Prisma Schema

Here is the model definition for the roles tables in my “schema.prisma” file

model roles {
  id      Int      @id @default(autoincrement())
  name    String
  addedOn DateTime @default(now())
}

And here is the complete schema.prisma file, with all the tables that we defined in the last stories

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider      = "prisma-client-js"
  binaryTargets = ["native", "debian-openssl-1.1.x"]
}

datasource db {
  provider     = "postgresql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"
}

model logins {
  id        Int      @id @default(autoincrement())
  email     String
  name      String
  logins    Int
  objectId  String?
  lastLogin DateTime @default(now())
}

model ApiToken {
  tokenId        String   @id @default(cuid())
  accessToken    String   @db.VarChar
  tokenType      String   @default("Azure")
  expirationDate DateTime
  addedOn        DateTime @default(now())
}

model roles {
  id      Int      @id @default(autoincrement())
  name    String
  addedOn DateTime @default(now())
}

Prisma Delete Record

To delete this new record, I use this code snippet.

data = await prisma.roles.delete({
      where: {
        id: id,
      },
    });

Here is the complete code from the component “promoteUser.component.tsx”, where I show the Users from Azure AD B2C directory and on each line a “Select” for the roles, which we defined via Prisma in our “roles” table and one button for the role change and a new button for “User Delete”. I am using the Table component from Tremor/React as a base structure and three different API calls for CRUD operations

# Deleting User on Azure AD B2C
/api/azure/user/delete

and

# Patch / Modify User on Azure AD B2C
/api/user/promote

and

# Get Azure AD B2C Users
/api/azure/user
"use client";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { useState, useEffect } from "react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";

import {
  Text,
  Title,
  Button,
  Table,
  TableHead,
  TableRow,
  TableHeaderCell,
  TableBody,
  TableCell,
} from "@tremor/react";

interface User {
  id: string;
  name: string;
  role: string;
  email: string;
  created: string;
}

interface Role {
  id: number;
  name: string;
  addedOn: Date;
}

let b2cextensionuservalue: string | unknown = "";

const PromoteRole = ({
  roles,
  b2cextensionuser,
  tld,
}: {
  roles: Role[];
  b2cextensionuser: string;
  tld: string;
}) => {
  const [usercomplete, setUsercomplete] = useState<User[]>([]);
  const [time, setTime] = useState(Date.now());
  const [value, setValue] = useState("");
  const locale = useParams()?.locale as LocaleTypes;
  const { t } = useTranslation(locale, "common");

  const ChangeHandler = (e: any) => {
    let role = e.target.value;
    // console.log("role:", role);
    setValue(role);
  };

  const ondelete = async (id: string) => {
    // fields check

    let response = await fetch("/api/azure/user/delete?userid=" + id);

    // // get the data
    let data = response.status;

    console.log("data:", data);

    if (data === 204) {
      // reset the fields

      toast.success("Deleted", {
        position: "top-right",
        autoClose: 1000,
      });
      //Set Time to trigger UseEffect
      setTime(Date.now());
    } else {
      toast.error(
        "Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
        {
          position: "top-right",
          autoClose: 1000,
        }
      );
    }
  };

  const onpromote = async (id: string, role: string) => {
    // user structure
    let user = {
      id,
      role,
    };

    if (!role) return toast.error(t("user.rolemissing"), { autoClose: 1000 });

    let response = await fetch("/api/user/promote", {
      method: "POST",
      body: JSON.stringify(user),
    });

    let data = response.status;

    if (data === 204) {
      toast.success(t("user.changedto") + " " + role, {
        position: "top-right",
        autoClose: 1000,
      });
      // reset the fields
      setValue("");

      setTime(Date.now());
    } else {
      toast.error(
        "Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
        {
          position: "top-right",
          autoClose: 1000,
        }
      );
    }
  };

  useEffect(() => { 
    const fetchUsers = async () => {
      const response = await fetch("/api/azure/user", {
        method: "GET",
      });

      // get the data
      let data = await response.json();   
      const users = data.data.value;
   
      const usercompletenew: any[] = [];

      users.forEach((user: any) => {
        b2cextensionuservalue = "";

        Object.entries(user).forEach((entry) => {
          const [key, value] = entry;
          if (key === b2cextensionuser) {           
            b2cextensionuservalue = value || "";
          }
        });
      
        usercompletenew.push({
          id: user.id,
          name: user.displayName,
          role: b2cextensionuservalue,
          email:
            user.identities[0].signInType === "emailAddress"
              ? user.identities[0].issuerAssignedId
              : user.otherMails[0],
          created: user.createdDateTime,
        });
      });

      setUsercomplete(usercompletenew);
    };

    fetchUsers();
  }, [time, b2cextensionuser]);

  return (
    <>
      <ToastContainer />
      <Title>{t("search.dashboardsearchhighline")}</Title>
      <Text>{t("search.dashboardsearchdescription")}</Text>
      <Table className="mt-4 border border-gray-700 rounded-md">
        <TableHead>
          <TableRow>
            <TableHeaderCell>Name</TableHeaderCell>
            <TableHeaderCell>{t("user.role")}</TableHeaderCell>
            <TableHeaderCell>Email</TableHeaderCell>
            <TableHeaderCell>{t("user.createdat")}</TableHeaderCell>
            <TableHeaderCell>Role Selection</TableHeaderCell>
            <TableHeaderCell>{t("user.actions")}</TableHeaderCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {usercomplete.map((datauseritem: User, index: number) => (
            <TableRow key={index}>
              <TableCell>
                <Text>{datauseritem.name}</Text>
              </TableCell>
              <TableCell>
                <Text>{datauseritem.role}</Text>
              </TableCell>
              <TableCell>
                <Text>{datauseritem.email}</Text>
              </TableCell>
              <TableCell>
                <Text>{datauseritem.created}</Text>
              </TableCell>
              <TableCell>
                <select
                  id={index.toString()}
                  className="bg-gray-500 border border-gray-500 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 hover:bg-gray-600 hover:border-gray-700"
                  onChange={(e) => ChangeHandler(e)}
                >
                  <option value="">{t("user.selectrole")}</option>
                  {roles.map((role: Role, index: number) => (
                    <option key={index} value={role.name || ""}>
                      {role.name}
                    </option>
                  ))}
                </select>
              </TableCell>
              <TableCell>
                <Button
                  size="sm"
                  variant="primary"
                  color="gray"
                  onClick={() => onpromote(datauseritem.id, value)}
                >
                  {t("user.promoteadmin")}
                </Button>
              </TableCell>
              {/* Delete user */}
              <TableCell>
                <Button
                  size="sm"
                  variant="primary"
                  color="gray"
                  onClick={() => ondelete(datauseritem.id)}
                >
                  {t("user.deleteuser")}
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </>
  );
};

export default PromoteRole;

API for Azure AD B2C User Deletion

If we have a valid Access Token in our ApiToken table, we use that one. Otherwise, we generate and save a new one. We can perform the delete call with the Token and the Azure AD B2C ID of the user object.

import type { AuthTokenResp } from "@/types/api";
import prisma from "@/lib/prisma";
import { unixTimestampToDate, urlBase64Decode } from "@/lib/utils";
import { NextResponse, NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";

async function AzureToken() {
  const formdata = new FormData();

  formdata.append("grant_type", "client_credentials");
  formdata.append("client_id", process.env.AZURE_CLIENT_ID || "");
  formdata.append("client_secret", process.env.AZURE_CLIENT_SECRET || "");
  formdata.append("scope", process.env.AZURE_SCOPE || "");

  if (!process.env.AZURE_TOKEN_URL) {
    throw new Error("AZURE_TOKEN_URL is not set");
  }

  const res = await fetch(process.env.AZURE_TOKEN_URL, {
    method: "POST",
    body: formdata,
  });

  if (!res.ok) {
    const text = await res.text();

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  const newTokens: AuthTokenResp = await res.json();

  return newTokens;
}

async function AzureData() {
  const mostRecentApiToken = await prisma.apiToken.findFirst({
    where: { tokenType: "Azure" },
    orderBy: {
      addedOn: "desc",
    },
  });

  let token: string = "";

  if (mostRecentApiToken) {
    const { accessToken, expirationDate } = mostRecentApiToken;

    const tokenExpirationDate = new Date(expirationDate);
    const currentDate = new Date();

    token = accessToken;

    // compare the current date to the expiration date
    if (currentDate > tokenExpirationDate) {
      //Get Access Token
      const tokenazure: any = await AzureToken();
      const newTokens = tokenazure;

      const accessToken = newTokens.access_token;
      const { exp, nbf } = urlBase64Decode(accessToken.split(".")[1]);
      const newExpirationDate = unixTimestampToDate(exp);

      await prisma.apiToken.create({
        data: {
          accessToken: accessToken,
          expirationDate: newExpirationDate,
          tokenType: "Azure",
          addedOn: new Date(),
        },
      });
    
      token = accessToken;
    }
  } else {
    //Get Access Token
    const tokenazure: any = await AzureToken();
    const newTokens = tokenazure;

    const accessToken = newTokens.access_token;
    const { exp, nbf } = urlBase64Decode(accessToken.split(".")[1]);
    const newExpirationDate = unixTimestampToDate(exp);

    await prisma.apiToken.create({
      data: {
        accessToken: accessToken,
        expirationDate: newExpirationDate,
        tokenType: "Azure",
        addedOn: new Date(),
      },
    });
   
    token = accessToken;
  }

  return token;
}

export async function GET(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  let data = null;

  const url = new URL(req.url);
  const userId = url.searchParams.get("userid");

  //Get Access Token
  const token = await AzureData();

  const GraphUrl = `https://graph.microsoft.com/v1.0/users/${userId}`;

  const res = await fetch(GraphUrl, {
    method: "DELETE",
    headers: {
      Authorization: "Bearer " + token,
    },
  });

  return new NextResponse(undefined, {
    status: res.status,
    statusText: res.statusText,
  });
}

Needed Azure AD B2C permissions for User deletion

We are using the service principal (app registration) “TestProd”, which I showed in this story.

However, the permission for user deletion is not given, so we have to adapt them.

To fix the issue, you need to assign your service principal to a directory role e.g. User administrator/ Global administrator.

Navigate to the Azure Active Directory in the Azure portal -> Roles and administrators -> click User administrator or Global administrator -> Add assignment -> search by your service principal name(must search) -> find it and select it -> click Select.

API for removing Role Records with Prisma

We pass the UserID from our Component, which we then use to delete the corresponding record from the Roles Table with Prisma.

import prisma from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";

export async function POST(req: Request) {
  // Extract the `messages` from the body of the request
  const { id } = await req.json();
  let data: any = null;

  if (id) {
    data = await prisma.roles.delete({
      where: {
        id: id,
      },
    });
  }

  if (data) {
    data["status"] = "success";
  }

  return NextResponse.json({
    data,
  });
}

Component for handling the roles in our Dashboard section

Below, you will find the component that we use to list the created roles. The component displays a “Delete” button on every row so that you can easily delete it.

"use client";
import {
  Table,
  TableHead,
  TableRow,
  TableHeaderCell,
  TableBody,
  TableCell,
  Badge,
  Button,
  Text,
} from "@tremor/react";
import type { LocaleTypes } from "@/app/i18n/settings";
import { useTranslation } from "@/app/i18n/client";
import { useParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

interface Role {
  id: number;
  name: string;
  addedOn: Date;
}

export default function RolesTable({ roles }: { roles: Role[] }) {
  const router = useRouter();
  let intlDateObj = new Intl.DateTimeFormat("de-DE", {
    dateStyle: "full",
    timeStyle: "long",
    timeZone: "Europe/Paris",
  });
  const locale = useParams()?.locale as LocaleTypes;
  const { t } = useTranslation(locale, "common");

  const ondelete = async (id: number) => {
    // user structure
    let user = {
      id,
    };

    let response = await fetch("/api/user/deleterole", {
      method: "POST",
      body: JSON.stringify(user),
    });

    //get the data
    let data = await response.json();

    if (data.data.status === "success") {      

      toast.success("Deleted", {
        position: "top-right",
        autoClose: 1000,
      });
      // Refresh page after User deletion
      router.refresh();
    } else {
      toast.error(
        "Oups, etwas ist schief gelaufen, bitte probieren Sie es noch einmal oder kontaktieren Sie uns.",
        {
          position: "top-right",
          autoClose: 1000,
        }
      );
    }
  };

  return (
    <>
      <Table>
        <TableHead>
          <TableRow>
            <TableHeaderCell>{t("user.roles")}</TableHeaderCell>
            <TableHeaderCell>{t("user.createdat")}</TableHeaderCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {roles.map((role: Role) => (
            <TableRow key={role.id}>
              <TableCell>
                <Text>{role.name}</Text>
              </TableCell>
              <TableCell>
                <Text>{intlDateObj.format(role.addedOn)}</Text>
              </TableCell>
              <TableCell>
                <Button
                  size="xs"
                  variant="primary"
                  color="gray"
                  onClick={() => ondelete(role.id)}
                >
                  Delete
                </Button>
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </>
  );
}

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Related articles