This documentation provides a complete end-to-end guide for building a modern identity demo. It covers the front-end interface, the Azure integration bridge, and the backend security analytics configuration.
This project simulates a secure login portal to generate telemetry. Unlike legacy systems, it uses a Data Collection Rule (DCR) ingestion path, ensuring that logs are natively indexed by Microsoft Defender XDR and visible in Advanced Hunting.
The DCE is the physical gateway for your logs.
- Search: Search for 'Data Collection Endpoints' in the Azure portal.
- Create: Deploy a new DCE in the same region as your Log Analytics Workspace.
- Logs Ingestion URL: Copy the URL from the Overview blade (e.g.,
https://...ingest.monitor.azure.com).
- Navigate: Log Analytics Workspace > Settings > Tables.
- Create: Select Create > New custom log (DCR-based).
- Name:
DemoSignInLogs(the_CLsuffix is added automatically). - DCR: Select Create new for the Data Collection Rule during this process.
- Schema: Upload the sample JSON (Section 5) to automatically define columns.
- Search: Search for 'Data Collection Rules' in the portal.
- Immutable ID: Open the JSON View of your DCR and copy the
immutableId. - Transformation: Go to Data Sources > Inbound Stream > Transformation Editor.
- KQL: Paste the following to map the mandatory
TimeGeneratedfield:source | extend TimeGenerated = todatetime(timestamp)
The Logic App requires authorization to write to the DCR.
- Resource: Go to your Data Collection Rule.
- Role: Assign Monitoring Metrics Publisher.
- Assignee: Select the System-assigned Managed Identity of your Logic App.
The Logic App securely handles data from the web and delivers it to the ingestion API.
- Method:
POST - URI:
https://{DCE-URL}/dataCollectionRules/{DCR-ID}/streams/Custom-DemoSignInLogs_CL?api-version=2023-01-01 - Headers:
Content-Type: application/json - Body Expression:
[ @triggerBody() ](Data must be sent as a JSON array). - Authentication: Managed Identity with Audience
https://monitor.azure.com/.
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"When_an_HTTP_request_is_received": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"username": { "type": "string" },
"pin": { "type": "string" },
"timestamp": { "type": "string" },
"clientIP": { "type": "string" },
"action": { "type": "string" }
}
}
}
}
},
"actions": {
"HTTP": {
"runAfter": {},
"type": "Http",
"inputs": {
"uri": "https://{DCE-URL}/dataCollectionRules/{DCR-ID}/streams/Custom-DemoSignInLogs_CL?api-version=2023-01-01",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": [ "@triggerBody()" ],
"authentication": {
"type": "ManagedServiceIdentity",
"audience": "https://monitor.azure.com/"
}
}
}
}
}
}The website features a Glassmorphism UI and captures the user's real public IP. It also enforces a 4-digit numeric PIN.
- IP Discovery: Calls
api.ipify.orgto fetch the actual public IP before ingestion. - PIN Enforcement: Regex prevents non-numeric input;
maxlength="4"limits length. - Auto-Reset: Clears the PIN field upon success while keeping the Username for repeated testing.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Azure Identity Demo</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root { --primary: #0078d4; --bg: #0a0a0a; --glass: rgba(255, 255, 255, 0.05); }
body { font-family: 'Inter', sans-serif; background: radial-gradient(circle at top right, #1a1a2e, var(--bg)); color: white; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
.login-card { background: var(--glass); backdrop-filter: blur(15px); border: 1px solid rgba(255, 255, 255, 0.1); padding: 40px; border-radius: 20px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); width: 320px; text-align: center; }
h3 { font-weight: 300; margin-bottom: 30px; letter-spacing: 1px; color: #ccc; }
.input-group { position: relative; margin-bottom: 20px; }
.input-group i { position: absolute; left: 15px; top: 50%; transform: translateY(-50%); color: #666; }
input { width: 100%; padding: 12px 15px 12px 45px; background: rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; color: white; outline: none; box-sizing: border-box; }
button { width: 100%; padding: 12px; background: var(--primary); border: none; border-radius: 8px; color: white; font-weight: 600; cursor: pointer; transition: all 0.3s; margin-top: 10px; }
button:hover { background: #005a9e; box-shadow: 0 0 15px rgba(0, 120, 212, 0.4); }
#status { margin-top: 20px; font-size: 12px; color: #aaa; }
</style>
</head>
<body>
<div class="login-card">
<i class="fa-solid fa-shield-halved" style="font-size: 40px; color: var(--primary); margin-bottom: 15px;"></i>
<h3>SECURE PORTAL</h3>
<div class="input-group">
<i class="fa-regular fa-user"></i>
<input type="text" id="username" placeholder="Username" required autocomplete="off">
</div>
<div class="input-group">
<i class="fa-solid fa-key"></i>
<input type="password" id="pin" placeholder="4-digit PIN" maxlength="4" inputmode="numeric" oninput="this.value = this.value.replace(/[^0-9]/g, '')" required>
</div>
<button onclick="sendData()" id="btnText">Sign In</button>
<p id="status"></p>
</div>
<script>
async function sendData() {
const userEl = document.getElementById('username');
const pinEl = document.getElementById('pin');
const statusEl = document.getElementById('status');
const btnText = document.getElementById('btnText');
const logicAppUrl = "YOUR_LOGIC_APP_URL";
if (!userEl.value || pinEl.value.length !== 4) { statusEl.innerHTML = '<span style="color: #ff4d4d;">Invalid credentials.</span>'; return; }
btnText.disabled = true; btnText.innerText = "Processing...";
try {
const ipRes = await fetch('https://api.ipify.org?format=json');
const ipData = await ipRes.json();
const payload = { username: userEl.value, pin: pinEl.value, timestamp: new Date().toISOString(), clientIP: ipData.ip, action: "LoginAttempt" };
const response = await fetch(logicAppUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (response.ok) { statusEl.innerHTML = 'Log ingested.'; pinEl.value = ""; } else { statusEl.innerText = "Failed."; }
} catch (e) { statusEl.innerText = "Connection error."; } finally { btnText.disabled = false; btnText.innerText = "Sign In"; }
}
</script>
</body>
</html>Use this to define your DCR schema:
{
"username": "Clippy",
"pin": "0987",
"timestamp": "2026-02-23T09:24:22.547Z",
"clientIP": "1.2.3.4",
"action": "LoginAttempt"
}DemoSignInLogs_CL
| project TimeGenerated, username, pin, clientIP, action
| order by TimeGenerated desc