Table of Contents
- First API Calls to App Service via Postman
- Full example to get the access token in Postman from Azure AD B2C
- Setting Redirect-URI in Azure APP Registration
- POST Request Body Post
- Providing Environment Variables to App Service
- New App Registration for Resource Creation
- Nextjs 14 logic for calling App Service
- Component - createstorageaccountform_appservice.component.tsx
- API Route CreateStorageAccountAppservice
- API Route DeleteStorageAccountAppservice
- API Route manageoperations
- Cloudapp-dev, and before you leave us
We created a Linux App Service instance in the F1 plan (Free) in the first story. In the second story, we secured the App Service to accept only authorized requests. In the third story, we will use our Next.js app to get the Azure AD B2C Access Token to create Resources on Azure via the App Service created initially.
I must admit that the ramp-up of the app service instance in the free plan is not very fast, so consider using a paid plan where the instance is “always on” if you use the service beyond the development.
First API Calls to App Service via Postman
Before integrating our Next.js project, I recommend making some test requests via Postman, which will significantly speed up the later development.
Full example to get the access token in Postman from Azure AD B2C
We create a new “POST” Request in Postman, and we use this endpoint: https://yourAppServiceName.azurewebsites.net/api/storage/create-storage-account
In the Authorization Tab, we select “OAuth 2.0” and add authorization data to “Request Headers”
You can define a name for the Token

Grant type -> we select “Authorization Code (With PKCE)”
Auth URL -> https://yourADB2CTenant.b2clogin.com/yourADB2CTenant.onmicrosoft.com/oauth2/v2.0/authorize?p=yourADB2CUserflow
Access Token URL -> https://yourADB2CTenant.b2clogin.com/yourADB2CTenant.onmicrosoft.com/oauth2/v2.0/token?p=yourADB2CUserflow
Client ID -> In the previous story, we created an App Registration, and we copied the Client ID (Application ID). Please reuse it here.
Code Challenge Method -> SHA-256
Scope -> openid offline_access
Client Authentication -> Send client credentials in body

If you click on the button “Get New Access Token” Postman will open a pop-up window with the default “Azure AD B2C User Flow Web Interface”, where we can enter the username and password to get the needed Access Token via Callback to https://oauth.pstmn.io/v1/browser-callback
Setting Redirect-URI in Azure APP Registration
To make this happen, we must add a new setting in the previously created App Registration. We click on “Authentication” -> “Add a platform” -> “Single Page Application,” and then we add the Postman URI.

POST Request Body Post
The Json in the Raw body should look like the example data below.
“user_az_id”: -> ObjectID from an existing Azure AD B2C User
“user_email”: -> Email from an existing Azure AD B2C User, which we will use to send a confirmation mail via Resend.com
{
"resourceGroupName": "myResourceSta",
"accountName": "cloudappstat12",
"location": "westeurope",
"containerName": "mycontainer",
"user_az_id": "xxx-xxxx-xxxx",
"user_email": "xxxxx@gmail.com",
"tags": {
"environment": "dev"
}
}With this JSON Body in place, APP Service will create the resource.
Providing Environment Variables to App Service
But wait. We forgot to provide the right environment variables to the App Service because since the logic is creating Azure Resources, we need an APP Registration with proper rights.

Here, we have to distinguish between the App Registration that we used to Secure the App Service from the outside and the App Registration that we will use to create Resources within our Azure Subscription.
Since the securing part was handled in the previous story, you can get all the details here:
https://www.cloudapp.dev/next-js-14-building-a-saas-solution-in-azure-authentication-part-2
New App Registration for Resource Creation
Let’s create a new app registration called “iac_azure.” For “Supported account types” select -> Accounts in this organizational directory only (xxxxx only — Single tenant)
Enable “Access tokens” and “ID tokens” in the “Authentication tab. No special setting in “API permissions” is needed. Create a new Client Secret under “Certificates & secrets”.
After creating the app registration, I added the service principal (app registration) to the section “Access control (IAM)” in the subscription with the “contributor” role.
With the new app registration, we have these four env variables:
AZURE_CLIENT_ID
AZURE_CLIENT_SECRET
AZURE_SUBSCRIPTION_ID
AZURE_TENANT_ID
The variable “aadb2c_AUTHENTICATION_SECRET” was automatically added when we set up the AD Auth for the App Service. DATABASE_URL is only needed if you want to save data to a Postgres DB; if not, remove the corresponding code from the logic. RESEND_API_KEY is required by the mail sent out after successfully creating the resource.
The logic takes the values from the AZURE env variables for the authentication.
Nextjs 14 logic for calling App Service
I start with the creation of a new component, followed by several API Routes.
Component - createstorageaccountform_appservice.component.tsx
With the function checkIfUserHasStorageAccount, I check if the user already has a storage account/resource group. If yes I show the button for deleting the resourcegroup/storage account, otherwise the user can create them.
With the function generateRandomName I generate Random names for the resourcegroup/storgeaccount.
I use the accesstoken from Azure AD B2C which I get with “usesession” -> const token = session?.accessToken;
"use client";
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import LoadingButton from "@/components/tools/loadingbutton/loadingbutton.component";
// Utility function to generate a random alphanumeric string of a specified length
const generateRandomName = (length: number) => {
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
};
const CreateStorageAccountFormAppService: React.FC = () => {
const { data: session } = useSession();
const user_az_id = session?.user?.sub;
const user_email = session?.user?.email;
//Get Access Token
const token = session?.accessToken;
const [resourceGroupName, setResourceGroupName] = useState(
generateRandomName(24)
); // Random default value
const [accountName, setAccountName] = useState(generateRandomName(24)); // Random default value
const [location, setLocation] = useState("westeurope"); // Default to "westeurope"
const [tags, setTags] = useState<{ [key: string]: string }>({
env: "production",
user: user_az_id || "",
}); // Default value with example tag
const [containerName, setContainerName] = useState("mycontainer"); // Default value
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [containers, setContainers] = useState<string[]>([]); // State to store fetched containers
const [azureops, setAzureOps] = useState(""); // State for Azure Operations
const [hasStorageAccount, setHasStorageAccount] = useState(false); // State to check if user already has a storage account
// Function to check if user already has a storage account
const checkIfUserHasStorageAccount = async () => {
if (!user_az_id) return; // No user ID available
try {
const response = await fetch(
"/api/azure/storageaccount/check-storage-account",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userId: user_az_id }),
}
);
if (response.ok) {
const data: { hasStorageAccount: boolean } = await response.json();
setHasStorageAccount(data.hasStorageAccount);
} else {
console.error("Error checking storage account existence.");
}
} catch (error) {
console.error("Error:", error);
}
};
// Funktion zum Abrufen des Werts aus der Datenbank
const fetchData = async () => {
try {
const operation = azureops;
const value = "status";
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName,
operation,
value,
}),
});
const data = await responseOps.json();
if (azureops === "create") {
console.log("Data: ", data.data.creation);
if (data.data.creation === "Success") {
setMessage("Storage account created successfully!");
setHasStorageAccount(true); // Update the state to reflect the creation
setLoading(false);
}
}
if (azureops === "delete") {
console.log("Data: ", data.data.deletion);
if (data.data.deletion === "Success") {
setMessage("Storage account successfully deleted!");
setHasStorageAccount(false); // Update the state to reflect the creation
setLoading(false);
}
}
} catch (error) {
console.error("Fehler beim Abrufen der Daten:", error);
}
};
useEffect(() => {
checkIfUserHasStorageAccount();
}, [session]);
// Überwacht Datenbankwertänderungen
useEffect(() => {
// Datenbankabfrage alle 5 Sekunden
if (loading === false) return; // Nur abfragen, wenn der Ladezustand true ist
const intervalId = setInterval(fetchData, 5000);
return () => clearInterval(intervalId); // Bereinigen, wenn die Komponente unmontiert wird
}, [loading === true]);
// Delete storage account
const handleDeleteStorageAccount = async () => {
if (!accountName) return; // No storage account name available
setLoading(true);
setMessage("");
setAzureOps("delete");
try {
// Creating Records in Operationstable
const operation = "delete";
const value = "pending";
// Call the function to create a record in the operation table
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName,
operation,
value,
}),
});
const response = await fetch(
"/api/azure/resources/deleteresourcegroupappservice",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
tagKey: "user",
tagValue: user_az_id,
resourceGroupName: resourceGroupName,
}),
}
);
if (response.ok) {
const successData = await response.text();
setMessage(`Success: ${successData}`);
} else {
const errorData = await response.text();
setMessage(`Error: ${errorData}`);
}
} catch (error) {
setMessage(`Error: ${error}`);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
setAzureOps("create");
setContainers([]); // Clear previous container list
// Check if storageaccount and/or Resource Group already exists
try {
// Creating Records in Operationstable
const operation = "create";
const value = "pending";
// Call the function to create a record in the operation table
const responseOps = await fetch("/api/azure/resources/manageoperations", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
resourceGroupName,
operation,
value,
}),
});
const response = await fetch(
"/api/azure/resources/createstorageaccountappservice",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
body: JSON.stringify({
resourceGroupName,
accountName,
location,
tags,
containerName,
user_az_id,
user_email,
}),
}
);
if (response.ok) {
const successData = await response.text();
setMessage(`Success: ${successData}`);
} else {
const errorData = await response.text();
setMessage(`Error: ${errorData}`);
}
} catch (error) {
setMessage(`Error: ${error}`);
}
};
return (
<div className="max-w-lg p-6 mx-auto mt-6 bg-gray-400 rounded-lg shadow-md">
<h2 className="mb-4 text-2xl font-bold text-center">
{hasStorageAccount
? "You already have a storage account"
: "Create Storage Account"}
</h2>
{/* Delete Storage Account Button */}
{hasStorageAccount && (
<LoadingButton
loading={loading}
onClick={handleDeleteStorageAccount}
startlabel="Deleting Storage Account"
loadinglabel="Deleting ..."
/>
)}
{!hasStorageAccount && (
<LoadingButton
loading={loading}
onClick={handleSubmit}
startlabel="Creating Storage Account"
loadinglabel="Creating ..."
/>
)}
{/* Message display */}
{message && (
<p className="mt-4 text-sm font-medium text-center">{message}</p>
)}
{/* Containers list display */}
{containers.length > 0 && (
<div className="mt-6">
<h3 className="mb-2 text-lg font-semibold">Containers:</h3>
<ul className="list-disc list-inside">
{containers.map((container) => (
<li className="text-gray-600" key={container}>
{container}
</li>
))}
</ul>
</div>
)}
</div>
);
};
export default CreateStorageAccountFormAppService;API Route CreateStorageAccountAppservice
As the Payload I send this data:
resourceGroupName,
accountName,
location,
containerName,
user_email,
user_az_id,
tags,import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
const authtoken = await getToken({ req });
const isAdmin = authtoken?.role === "Admin";
if (!authtoken || !isAdmin) {
return NextResponse.json(
{ error: "Unauthorized Create Storage API" },
{ status: 401 }
);
}
// Respond with a success message
const accessToken = authtoken?.accessToken;
if (!accessToken) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const {
resourceGroupName,
accountName,
location,
containerName,
user_email,
user_az_id,
tags,
} = await req.json();
// Payload to send
const payload: any = {
resourceGroupName,
accountName,
location,
tags,
containerName,
user_email,
user_az_id,
};
// Convert the payload to a JSON string
const message = payload;
if (!message) {
return NextResponse.json(
{ error: "Message body is required." },
{ status: 400 }
);
}
// Rest API URL Service Bus
const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/create-storage-account`;
try {
// Send message to Service Bus Queue using Azure REST API
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(payload),
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error ServiceBus! status: ${response.status}`);
}
// Check if the response body exists and is not empty
const responseText = await response.text();
if (responseText) {
return NextResponse.json({ message: "Message sent successfully" });
// const data = JSON.parse(responseText);
// return data;
} else {
// console.warn("No response body received");
// return NextResponse.json({ message: responseText });
return NextResponse.json({
message: "Message sent successfully via Text" + responseText,
});
}
// I removed resopnse.json() because it was causing an error, since the StatusCode is 201 and there is no response body
// const data = await response.json();
} catch (err) {
console.error("Error sending message to App Service", err);
return NextResponse.json(
{ error: "Error sending message to App Service" },
{ status: 500 }
);
}
} catch (err) {
console.error("Error processing request:", err);
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
}API Route DeleteStorageAccountAppservice
As the payload, I send the Resourcegroupname, TagKey, and TagValue
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
const authtoken = await getToken({ req });
const isAdmin = authtoken?.role === "Admin";
if (!authtoken || !isAdmin) {
return NextResponse.json(
{ error: "Unauthorized Create Storage API" },
{ status: 401 }
);
}
// Respond with a success message
const accessToken = authtoken?.accessToken;
if (!accessToken) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const { tagKey, tagValue, resourceGroupName } = await req.json();
// Payload to send
const payload: any = {
tagKey,
tagValue,
resourceGroupName,
};
// Convert the payload to a JSON string
const message = payload;
if (!message) {
return NextResponse.json(
{ error: "Message body is required." },
{ status: 400 }
);
}
// Rest API URL Service Bus
const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/delete-resource-groups-by-tag`;
try {
const response = await fetch(url, {
method: "DELETE",
body: JSON.stringify(payload),
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Check if the response body exists and is not empty
const responseText = await response.text();
if (responseText) {
return NextResponse.json({ message: "Message sent successfully" });
// const data = JSON.parse(responseText);
// return data;
} else {
// console.warn("No response body received");
return NextResponse.json({ message: responseText });
// return NextResponse.json({ message: "Message sent successfully" });
}
} catch (err) {
console.error("Error sending message to App Service:", err);
return NextResponse.json(
{ error: "Error sending message to App Service" },
{ status: 500 }
);
}
} catch (err) {
console.error("Error processing request:", err);
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
}API Route manageoperations
It is used to interact with the Postgres DB via Prisma
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import prisma_storageaccount from "@/lib/prisma_storageaccounts";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
const authtoken = await getToken({ req });
const isAdmin = authtoken?.role === "Admin";
if (!authtoken || !isAdmin) {
return NextResponse.json(
{ error: "Unauthorized Operations API" },
{ status: 401 }
);
}
try {
const { resourceGroupName, operation, value } = await req.json();
if (operation == "") {
return NextResponse.json(
{ error: "Invalid request. Operation is missing" },
{ status: 400 }
);
}
const creationvalue = operation === "create" ? value : "NoOps"; // If the operation is create, set the creation value to the value, otherwise set it to null
const deleteionvalue = operation === "delete" ? value : "NoOps"; // If the operation is delete, set the deletion value to the value, otherwise set it to null
const operationGetResult = await prisma_storageaccount.operations.findFirst(
{
where: {
resourcegroup: resourceGroupName,
},
}
);
// If value is status, return the operation result
if (value === "status") {
return NextResponse.json({ data: operationGetResult }, { status: 200 });
}
let operationResult: any = "";
if (!operationGetResult) {
operationResult = await prisma_storageaccount.operations.create({
data: {
resourcegroup: resourceGroupName,
creation: creationvalue,
deletion: deleteionvalue,
},
});
} else {
if (operation === "create") {
operationResult = await prisma_storageaccount.operations.update({
where: {
id: operationGetResult.id,
},
data: {
creation: creationvalue,
},
});
} else if (operation === "delete") {
operationResult = await prisma_storageaccount.operations.update({
where: {
id: operationGetResult.id,
},
data: {
deletion: deleteionvalue,
},
});
}
}
return NextResponse.json({ message: operationResult }, { status: 200 });
} catch (err) {
console.error("Error processing request:", err);
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
}
}I integrated the component into a “dashboard” page where the Next-Auth integration with AZURE AD B2C was already present, so I could easily use the UserID, EmailAddress, AccessToken, etc.
Now, we have a nice-looking component for creating and deleting Azure Resources, which you can extend as you want.
Here is the GitHub Repo for the AppService
https://github.com/cloudapp-dev/azure-app-service-iac
Here is the GitHub Repo for the front-end
https://github.com/cloudapp-dev/nextjs14-advanced-algoliasearch/tree/nextjs1
Play around with the code, and stay tuned for the next part of the tutorial.
Cloudapp-dev, and before you leave us
Thank you for reading until the end. Before you go:


