param(
[Parameter(Mandatory = $false)]
[Alias('From')]
[AllowNull()][object]$FromDate,
[Parameter(Mandatory = $false)]
[Alias('To')]
[AllowNull()][object]$ToDate,
[string]$OutputPath = ".",
[string]$TeamsWebhookUrl = ''
)
# Configuration
$organization = "organization"
$project = "project"
$iterationPath = "iterationPath"
$TeamsWebhookUrl = "TeamsWebhookUrl"
# Default to today if no dates provided
if (-not $ToDate) { $ToDate = Get-Date }
if (-not $FromDate) { $FromDate = $ToDate }
# Normalize to Date objects
if ($ToDate -is [string]) { $ToDate = Get-Date $ToDate }
if ($FromDate -is [string]) { $FromDate = Get-Date $FromDate }
# Use date-only bounds for today
$fromDay = $FromDate.Date
$toDay = $ToDate.Date.AddDays(1).AddSeconds(-1)
Write-Host "Collecting burned hours for tasks between $($fromDay.ToString('yyyy-MM-dd')) and $($toDay.ToString('yyyy-MM-dd'))" -ForegroundColor Cyan
# Acquire ADO access token via Azure CLI
try {
$accessToken = az account get-access-token --resource https://app.vssps.visualstudio.com --query accessToken -o tsv
} catch {
Write-Error "Failed to get Azure access token. Ensure 'az' is installed and you are logged in (az login)."
exit 1
}
# WIQL to find tasks in the iteration
$wiql = @"
SELECT [System.Id]
FROM WorkItems
WHERE [System.WorkItemType] = 'Task'
AND [System.IterationPath] = '$iterationPath'
"@
$wiqlBody = @{ query = $wiql } | ConvertTo-Json
$wiqlUrl = "https://dev.azure.com/$organization/$project/_apis/wit/wiql?api-version=6.0"
$wiqlResult = Invoke-RestMethod -Method Post -Uri $wiqlUrl -Headers @{ Authorization = "Bearer $accessToken" } -Body $wiqlBody -ContentType "application/json"
if (-not $wiqlResult.workItems -or $wiqlResult.workItems.Count -eq 0) {
Write-Host "No tasks changed in the specified date range." -ForegroundColor Yellow
return
}
$ids = $wiqlResult.workItems | Select-Object -ExpandProperty id
$batchIds = $ids -join ','
$fieldsList = "System.Id,System.AssignedTo"
$detailsUrl = "https://dev.azure.com/$organization/$project/_apis/wit/workitems?ids=$batchIds&fields=$fieldsList&api-version=6.0"
$detailsResult = Invoke-RestMethod -Method Get -Uri $detailsUrl -Headers @{ Authorization = "Bearer $accessToken" }
if (-not $detailsResult -or -not $detailsResult.value) {
Write-Host "No work item details returned from ADO." -ForegroundColor Yellow
return
}
# Prepare output
$out = @()
$todayString = $fromDay.ToString('yyyy-MM-dd')
foreach ($item in $detailsResult.value) {
$workItemId = $item.id
$assigned = if ($item.fields.'System.AssignedTo') { $item.fields.'System.AssignedTo'.displayName } else { 'Unassigned' }
# Get revisions for this work item and compute CompletedWork delta within the day
$revisionsUrl = "https://dev.azure.com/$organization/$project/_apis/wit/workItems/$workItemId/revisions?api-version=6.0"
try {
$revs = Invoke-RestMethod -Method Get -Uri $revisionsUrl -Headers @{ Authorization = "Bearer $accessToken" }
} catch {
$errMsg = if ($_.Exception) { $_.Exception.Message } else { $_.ToString() }
Write-Warning ("Failed to fetch revisions for {0}: {1}" -f $workItemId, $errMsg)
continue
}
# Ensure revisions were returned
if (-not $revs -or -not $revs.value) { continue }
# To include closed tasks where CompletedWork was updated at close, compute delta between
# the revision just before the window start (or earliest known) and the last revision up to window end.
$allRevs = $revs.value | Where-Object { $_.fields -and $_.fields.'System.ChangedDate' } | Sort-Object { Get-Date $_.fields.'System.ChangedDate' }
if (-not $allRevs -or $allRevs.Count -eq 0) { continue }
# Find the last revision strictly before the window start (baseline)
$baselineRev = $allRevs | Where-Object { (Get-Date $_.fields.'System.ChangedDate') -lt $fromDay } | Select-Object -Last 1
# If none, use the first revision available as baseline
if (-not $baselineRev) { $baselineRev = $allRevs[0] }
# Find the last revision at or before the window end
$endRev = $allRevs | Where-Object { (Get-Date $_.fields.'System.ChangedDate') -le $toDay } | Select-Object -Last 1
if (-not $endRev) { continue }
$firstCompleted = 0.0
$lastCompleted = 0.0
if ($baselineRev.fields.'Microsoft.VSTS.Scheduling.CompletedWork') { $firstCompleted = [double]$baselineRev.fields.'Microsoft.VSTS.Scheduling.CompletedWork' }
if ($endRev.fields.'Microsoft.VSTS.Scheduling.CompletedWork') { $lastCompleted = [double]$endRev.fields.'Microsoft.VSTS.Scheduling.CompletedWork' }
$burned = [math]::Round(($lastCompleted - $firstCompleted), 2)
if ($burned -le 0) { continue }
$out += [PSCustomObject]@{
TodayDate = $todayString
TaskId = $workItemId
BurnedHrs = $burned
AssignedTo = $assigned
LastChangedBy = if ($endRev.fields.'System.ChangedBy') { $endRev.fields.'System.ChangedBy'.displayName } else { '' }
}
}
if ($out.Count -eq 0) {
Write-Host "No burned hours recorded for tasks today." -ForegroundColor Yellow
return
}
$csvFile = Join-Path $OutputPath ("today-burned-tasks-{0}.csv" -f (Get-Date -Format "yyyyMMddHHmmss"))
$outSorted = $out | Sort-Object -Property AssignedTo
$outSorted | Export-Csv -Path $csvFile -NoTypeInformation
Write-Host "Report generated: $csvFile" -ForegroundColor Green
# If a Teams incoming webhook URL was provided, post the report content (not the file)
if ($TeamsWebhookUrl -and $TeamsWebhookUrl.Trim() -ne '') {
try {
# Build a concise text message. Limit displayed rows to avoid extremely long messages.
$maxLines = 50
$lines = $outSorted | ForEach-Object { "Task $($_.TaskId): $($_.BurnedHrs) hrs - $($_.AssignedTo)" }
if ($lines.Count -gt $maxLines) {
$displayLines = $lines[0..($maxLines - 1)] + ("...and $($lines.Count - $maxLines) more tasks")
} else {
$displayLines = $lines
}
# Build a monospaced table for Teams: use a code block so formatting is preserved
# Determine column widths based on data (with some caps)
$maxTaskIdWidth = 10
$maxBurnWidth = 8
$maxAssignWidth = 30
function Format-Row($tid, $burn, $assignee) {
$t = $tid.ToString()
if ($t.Length -gt $maxTaskIdWidth) { $t = $t.Substring(0,$maxTaskIdWidth) }
$b = $burn.ToString()
if ($b.Length -gt $maxBurnWidth) { $b = $b.Substring(0,$maxBurnWidth) }
$a = $assignee.ToString()
if ($a.Length -gt $maxAssignWidth) { $a = $a.Substring(0,$maxAssignWidth) }
return ("{0,-$maxTaskIdWidth} | {1,-$maxBurnWidth} | {2,-$maxAssignWidth}" -f $t, $b, $a)
}
$tableLines = @()
$tableLines += ("Burned hours report for $todayString")
$tableLines += ("Total tasks: $($outSorted.Count)")
$tableLines += ''
$tableLines += ("{0,-$maxTaskIdWidth} | {1,-$maxBurnWidth} | {2,-$maxAssignWidth}" -f 'TaskId','Burned','AssignedTo')
$tableLines += ('-' * ($maxTaskIdWidth + 3 + $maxBurnWidth + 3 + $maxAssignWidth))
foreach ($line in $displayLines) {
# displayLines formatted like: "Task 2404990: 3.5 hrs - Alice Smith"
if ($line -match '^Task\s+(\d+):\s+([0-9.]+)\s+hrs\s+-\s+(.*)$') {
$tid = $matches[1]
$burn = $matches[2]
$assignee = $matches[3]
$tableLines += Format-Row $tid $burn $assignee
} else {
# fallback: show the raw line truncated to fit
$raw = $line
if ($raw.Length -gt ($maxTaskIdWidth + $maxBurnWidth + $maxAssignWidth + 10)) { $raw = $raw.Substring(0, ($maxTaskIdWidth + $maxBurnWidth + $maxAssignWidth + 7)) + '...' }
$tableLines += $raw
}
}
if ($lines.Count -gt $maxLines) { $tableLines += "...and $($lines.Count - $maxLines) more tasks" }
# Wrap the table in a code block so Teams preserves monospace formatting
$bodyText = @"
$($tableLines -join "`n")
"@
$payload = @{ text = $bodyText } | ConvertTo-Json -Depth 4
Invoke-RestMethod -Method Post -Uri $TeamsWebhookUrl -ContentType 'application/json' -Body $payload -ErrorAction Stop
Write-Host "Posted report content to Teams webhook." -ForegroundColor Green
} catch {
$err = if ($_.Exception) { $_.Exception.Message } else { $_.ToString() }
Write-Warning "Failed to post report to Teams webhook: $err"
}
} else {
Write-Host "No Teams webhook URL provided; skipping Teams post." -ForegroundColor Gray
}
Microsoft Power Platform, SharePoint, Azure, AWS, Google Cloud, DevOps, AI/ML
Microsoft Power Platform, SharePoint, Azure, AWS, Google Cloud, DevOps, AI/ML
Monday, September 15, 2025
Automating Azure DevOps Task Tracking: A Complete PowerShell Solution
Tuesday, September 2, 2025
Building Secure APIs with FastAPI and Azure AD Authentication
Building Secure APIs with FastAPI and Azure AD Authentication
Published on September 2, 2025
In today's world of microservices and API-first architecture, securing your endpoints is crucial. In this blog post, we'll explore how to create a simple yet secure FastAPI application that integrates with Azure Active Directory (Azure AD) for authentication using bearer tokens.
What We're Building
Our FastAPI application demonstrates:
- Bearer token authentication using JWT tokens from Azure AD
- Token validation without signature verification (for demonstration purposes)
- User information extraction from the JWT payload
- Protected endpoints that require valid authentication
The Architecture
The application consists of a single FastAPI server with:
- A token verification function that validates Azure AD JWT tokens
- A protected
/hello
endpoint that returns user information - Integration with Azure AD using tenant-specific configuration
Key Components
1. Azure AD Configuration
TENANT_ID = "6213dbca-6fe9-42b2-bdaa-ccf2fc2f6332"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
APP_ID_URI = "b1c47286-f990-408a-8d6e-938377129947"
CLIENT_ID = "b1c47286-f990-408a-8d6e-938377129947"
SECRET_KEY = "APP Secret Key"
These configuration values connect your application to a specific Azure AD tenant and define the valid audiences for token validation.
2. Token Verification
The verify_token
function is the heart of our authentication system:
- Extracts bearer tokens from the Authorization header
- Decodes JWT tokens without signature verification (⚠️ Note: In production, always verify signatures!)
- Validates the audience to ensure tokens are intended for this application
- Handles common JWT errors like expired or malformed tokens
3. Protected Endpoints
Our /hello
endpoint demonstrates how to:
- Require authentication using FastAPI's dependency injection
- Extract user information from the token payload
- Return personalized responses based on the authenticated user
What Makes This Special
- Simple Integration: Just a few lines of code to secure your endpoints
- Enterprise Ready: Uses Azure AD, which is widely adopted in corporate environments
- User Context: Extracts meaningful user information like username and employee ID
- Scope Awareness: Can handle OAuth scopes for fine-grained permissions
Running the Application
Getting started is straightforward:
# Install dependencies
pip install fastapi uvicorn python-jose[cryptography]
# Run the server
uvicorn main:app --reload --host 0.0.0.0 --port 8000
from fastapi import FastAPI, Depends, HTTPException, statusfrom fastapi.security import HTTPBearer, HTTPAuthorizationCredentialsimport uvicornimport jwt
app = FastAPI(title="FastAPI Authentication Example")security = HTTPBearer()
TENANT_ID = "6213dbca-6fe9-42b2-bdaa-ccf2fc2f6332"AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"APP_ID_URI = "b1c47286-f990-408a-8d6e-938377129947" CLIENT_ID = "b1c47286-f990-408a-8d6e-938377129947"SECRET_KEY = "APP Secret Key"VALID_AUDIENCES = [APP_ID_URI, CLIENT_ID]
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): """Verify the bearer token using JWT validation""" try: decoded = jwt.decode(credentials.credentials, options={"verify_signature": False}) audience = decoded.get("aud") if audience not in VALID_AUDIENCES: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token audience", headers={"WWW-Authenticate": "Bearer"}, ) return decoded except jwt.ExpiredSignatureError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired", headers={"WWW-Authenticate": "Bearer"}, ) except jwt.InvalidTokenError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token format", headers={"WWW-Authenticate": "Bearer"}, )
@app.get("/hello")async def hello_world(decoded_token: dict = Depends(verify_token)): """Protected hello world endpoint""" username = decoded_token.get("preferred_username", "Unknown User") employee_id = decoded_token.get("employeeid", "Unknown") return { "message": "Hello World! You are authenticated.", "user": username, "employee_id": employee_id, "scopes": decoded_token.get("scp", "").split(" ") }
if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
The application will be available at http://localhost:8000
, with automatic API documentation at http://localhost:8000/docs
.
Testing the Authentication
To test the protected endpoint, you'll need a valid Azure AD token:
curl -H "Authorization: Bearer YOUR_AZURE_AD_TOKEN" \
http://localhost:8000/hello
A successful response will look like:
{
"message": "Hello World! You are authenticated.",
"user": "john.doe@company.com",
"employee_id": "EMP123",
"scopes": ["read", "write"]
}
Security Considerations
While this example demonstrates the basics, remember these important points for production use:
- Always verify token signatures - Don't set
verify_signature: False
in production - Use HTTPS - Never transmit tokens over unencrypted connections
- Validate all claims - Check issuer, expiration, and other relevant claims
- Implement rate limiting - Protect against abuse and brute force attacks
- Log security events - Monitor authentication failures and suspicious activity
Conclusion
FastAPI's elegant dependency injection system makes it incredibly easy to add authentication to your APIs. Combined with Azure AD's robust identity platform, you can quickly build secure, enterprise-ready applications.
This example provides a solid foundation that you can extend with additional features like:
- Role-based authorization
- API key authentication
- Multi-tenant support
- Advanced logging and monitoring
The complete code is available in this repository, ready for you to experiment with and adapt to your specific needs.
Ready to secure your APIs? Start with this example and build from there. FastAPI and Azure AD make a powerful combination for modern web applications.
Technologies Used
- FastAPI - Modern, fast web framework for building APIs
- Azure Active Directory - Microsoft's cloud-based identity service
- JWT - JSON Web Tokens for secure information transmission
- Python-JOSE - JWT library for Python
- Uvicorn - Lightning-fast ASGI server
Resources
- FastAPI Documentation
- Azure AD Authentication Documentation
- JWT.io - JWT debugger and documentation
Friday, August 29, 2025
Building a Read-Only Microsoft Information Protection Sensitivity Label Reader in C#
https://learn.microsoft.com/en-us/information-protection/develop/quick-app-initialization-csharp
Introduction
In today's data-driven world, protecting sensitive information is paramount. Microsoft Information Protection (MIP) provides robust capabilities for classifying, labeling, and protecting documents across your organization. While many solutions focus on applying and modifying sensitivity labels, there's often a need for read-only operations—such as auditing, compliance checking, or simply understanding what labels are applied to existing documents.
In this article, we'll explore how to build a simple yet powerful C# console application that reads Microsoft Information Protection sensitivity labels from files without modifying them. This solution is perfect for compliance officers, IT administrators, or developers who need to audit document classifications safely.
What We'll Build
Our application will:
- Connect to Microsoft Information Protection services
- List all available sensitivity labels in your organization
- Read existing sensitivity labels from specific files
- Display detailed label information including protection status
- Operate in read-only mode, ensuring no modifications to files
Prerequisites
Before we start, ensure you have:
- Azure AD App Registration with appropriate MIP permissions
- Visual Studio or .NET Framework 4.8 development environment
- Microsoft Information Protection SDK (via NuGet)
- Microsoft Authentication Library (MSAL) for authentication
- Files with sensitivity labels applied for testing
Azure AD App Registration Setup
First, you'll need to register an application in Azure Active Directory:
- Navigate to the Azure Portal → Azure Active Directory → App registrations
- Click "New registration"
- Provide a name (e.g., "Sensitivity Label Reader")
- Set redirect URI to
http://localhost
(for desktop app) - Under API permissions, add:
UnifiedPolicy.User.Read
(for reading label policies)InformationProtectionPolicy.Read
(for accessing protection policies)
Project Setup and Dependencies
Create a new console application and install the required NuGet packages:
<PackageReference Include="Microsoft.InformationProtection.File" Version="1.17.158" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.76.0" />
The Complete Implementation
Here's our streamlined, single-file implementation:
using Microsoft.InformationProtection;using Microsoft.InformationProtection.File;using Microsoft.Identity.Client;using System;using System.Linq;using System.Threading.Tasks;
namespace ConsoleApp5{ internal class Program { private const string clientId = "clientId"; private const string tenantId = "tenantId"; private const string appName = "SensitivityLabelReader"; private const string userEmail = "user1@demain.com"; private const string filePath = "C:\\Users\\TestdDoc.docx";
static void Main(string[] args) { try { // Initialize MIP SDK MIP.Initialize(MipComponent.File);
// Create application info var appInfo = new ApplicationInfo() { ApplicationId = clientId, ApplicationName = appName, ApplicationVersion = "1.0.0" };
// Create delegates var authDelegate = new AuthDelegateImplementation(appInfo, tenantId); var consentDelegate = new ConsentDelegateImplementation();
// Setup MIP context and profile var mipConfiguration = new MipConfiguration(appInfo, "mip_data", Microsoft.InformationProtection.LogLevel.Error, false, CacheStorageType.OnDiskEncrypted); var mipContext = MIP.CreateMipContext(mipConfiguration); var profileSettings = new FileProfileSettings(mipContext, CacheStorageType.OnDiskEncrypted, consentDelegate); var fileProfile = Task.Run(async () => await MIP.LoadFileProfileAsync(profileSettings)).Result;
// Setup engine var engineSettings = new FileEngineSettings(userEmail, authDelegate, "", "en-US") { Identity = new Identity(userEmail) }; var fileEngine = Task.Run(async () => await fileProfile.AddEngineAsync(engineSettings)).Result;
// Display available labels Console.WriteLine("Available Sensitivity Labels:"); Console.WriteLine("=============================="); foreach (var label in fileEngine.SensitivityLabels) { Console.WriteLine($"{label.Name} : {label.Id}"); foreach (var child in label.Children) { Console.WriteLine($"\t{child.Name} : {child.Id}"); } } Console.WriteLine();
// Read label from file Console.WriteLine($"Reading sensitivity label from: {filePath}"); Console.WriteLine("===============================================");
var handler = Task.Run(async () => await fileEngine.CreateFileHandlerAsync(filePath, filePath, true)).Result; var contentLabel = handler.Label;
if (contentLabel?.Label != null) { Console.WriteLine($"Label Name: {contentLabel.Label.Name}"); Console.WriteLine($"Label ID: {contentLabel.Label.Id}"); Console.WriteLine($"Is Protected: {contentLabel.IsProtectionAppliedFromLabel}"); if (!string.IsNullOrEmpty(contentLabel.Label.Description)) Console.WriteLine($"Description: {contentLabel.Label.Description}"); if (contentLabel.Label.Parent != null) Console.WriteLine($"Parent Label: {contentLabel.Label.Parent.Name}"); } else { Console.WriteLine("No sensitivity label found on this file."); } } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
Console.WriteLine("\nPress any key to exit..."); Console.ReadKey(); } }
// Authentication delegate for MIP SDK public class AuthDelegateImplementation : IAuthDelegate { private readonly ApplicationInfo _appInfo; private readonly string _tenantId;
public AuthDelegateImplementation(ApplicationInfo appInfo, string tenantId) { _appInfo = appInfo; _tenantId = tenantId; }
public string AcquireToken(Identity identity, string authority, string resource, string claims) { var authorityUri = new Uri(authority); authority = $"https://{authorityUri.Host}/{_tenantId}";
var app = PublicClientApplicationBuilder .Create(_appInfo.ApplicationId) .WithAuthority(authority) .WithDefaultRedirectUri() .Build();
var accounts = app.GetAccountsAsync().GetAwaiter().GetResult(); var scopes = new[] { resource.TrimEnd('/') + "/.default" };
var result = app.AcquireTokenInteractive(scopes) .WithAccount(accounts.FirstOrDefault()) .WithPrompt(Prompt.SelectAccount) .ExecuteAsync() .GetAwaiter() .GetResult();
return result.AccessToken; } }
// Consent delegate for MIP SDK public class ConsentDelegateImplementation : IConsentDelegate { public Consent GetUserConsent(string url) => Consent.Accept; }}
Featured Post
Automating Azure DevOps Task Tracking: A Complete PowerShell Solution
Automating Azure DevOps Task Tracking: A Complete PowerShell Solution param ( [ Parameter ( Mandatory = $false )] [ Alias ( 'Fr...
Popular posts
-
CAML:- --------- <Where> <IsNotNull> <FieldRef Name='ID' /> </IsNotNull> ...
-
WebForm1.aspx:- ------------------------- <% @ Page Language ="C#" AutoEventWireup ="true" CodeBehind ="...
-
This operation can be performed only on a computer that is joined to a server farm by users who have permissions in SQL Server to read from...
-
Authorize Postman to access SharePoint 1. Register Add-In 2. Grant Permissions to Add-In 3. Generate the Access Token 4. Access th...
-
CreateDocumentSet.aspx <%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %> <%@ Import Namesp...
-
Reading an excel file using HTML 5 and jQuery and save in SharePoint list Reference https://github.com/SheetJS/js-xlsx https://github.co...
-
Read SharePoint list Items using REST API and ReactJS Step1: Create a "ReactDemo" list and list columns as shown below. ...
-
Read excel data from document library saving as list items using CSOM in SharePoint. 1. Upload an excel file into Document library. This ...
-
CAML Query filter between two dates Method 1:- </And><Geq><FieldRef Name='Created' /><Value Type='Date...