Lately Iβve been focussing a lot on PowerShell related blogs and topics, so I thought it would be a good moment to go back to some Azure stuff again (and of course with a touch of PowerShell π). So here it is! My latest contribution which hopefully will help you strengthen your security whenever you are using databases with front-end services!
If you’ve been following my blog, you know I’m passionate about event-driven architecture and serverless solutions. Today, we’re diving into something equally important: passwordless authentication for databases! πIn modern cloud environments, storing database credentials in connection strings is a security risk.
Passwords can be leaked, stolen, or accidentally committed to source control. What if I told you that you can connect to Azure SQL Database without storing a single password?In this blog, we’ll explore how to use Azure Managed Identity with PowerShell Azure Functions to securely access Azure SQL Database – no connection strings, no passwords, just pure Azure AD token-based authentication!
Time for some serverless security automation!
During this post, you’ll see this icon π¬, which indicates that action is required so you get the full benefit out of this post.And I’ve introduced the π, which indicates that the subsequent part explains the technical details in the context of this post.
When to utilize this
Why go through the effort of setting up managed identity when connection strings “just work”? Well… connection strings do work, but they come with significant security challenges:
π΄ Credentials in code/config – Easy to leak or commit to repositories
π΄ Manual rotation – Requires updating all applications when passwords change
π΄ Broad access – Connection strings often have more permissions than needed
π΄ No audit trail – Hard to track which identity accessed what
Managed Identity solves all of this:
β No credentials stored – Azure AD handles authentication
β Automatic token rotation – Azure manages the lifecycle
β Granular permissions – Each function gets only what it needs
β Full audit trail – Every database access is logged with the identity
π§ And! This is a practical example of implementing zero-trust architecture in Azure. If you’re preparing for production workloads or want to follow security best practices, this is the way forward!
π Use cases: This pattern is perfect for web APIs, background processing functions, data migration tools, reporting services, or any serverless workload that needs database access.
π‘ In this blog Iβll be using Azure functions, but you can use whatever type of resource which supports managed identity and somehow can query a SQL server for this π
The Architecture
Here’s what we’re building:

What gets deployed:
- Azure SQL Server with Azure AD authentication
- SQL Database with sample data
- Azure Function App (PowerShell 7.4) with managed identity
- Storage Account for function runtime
- Application Insights for monitoring
- CORS pre-configured for Azure Portal testing
Setting up the Azure components
You have two options: deploy manually or let me handle it for you. I’ll provide both!
Option 1: Let me deploy it for you π
When deploying, you’ll need to provide:
- SQL Server Name – Globally unique (e.g., mycompany-sql-prod)
- SQL Admin Username – For initial setup only
- SQL Admin Password – Strong password (8+ chars, mixed case, numbers, symbols)
- Function App Name – Globally unique (e.g., mycompany-func-prod)
- Storage Account Name – Globally unique, lowercase, no hyphens (e.g., mycompanystorage123)
π The SQL admin credentials are only used for initial setup and emergency access. The function will never use these credentials, it uses managed identity tokens instead!
Don’t forget the name of the function! You need it later on
Example:

Option 2: Manual deployment
If you prefer manual deployment, create:
- Azure SQL Server (with firewall rule for Azure services)
- SQL Database
- Function App (with system-assigned managed identity enabled)
- Storage Account
- Application Insights
Oh, and donβt forget to configure CORS for portal.azure.com so that you can test in the end your configuration!
Configuring Azure AD Authentication
Here’s where the magic happens! We need to enable Azure AD authentication on SQL Server.
π¬ Step 1: Set an Azure AD Administrator on the SQL Server
- Go to Azure Portal
- Navigate to SQL servers β [Your SQL Server]
- Click Microsoft Entra ID in the left menu
- Click Set admin at the top
- Search for your user account
- Select it and click Select
- Set βSupport only Microsoft Entra Authentication for this serverβ
- Click Save
π This step enables Azure AD authentication on the SQL Server. Without this, the server won’t accept Azure AD tokens, only SQL authentication!

Granting Database Access to Managed Identity
Now comes the critical step: giving the Function App’s managed identity permission to access the database.
π¬ Step 2: Connect to the Database
- Go to SQL databases β [Your Database]
- Click Query editor in the left menu
- Login using Azure Active Directory authentication
- You’ll be connected as the Azure AD admin
π¬ Run the query below
CREATE USER [your-function-app-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [your-function-app-name];
ALTER ROLE db_datawriter ADD MEMBER [your-function-app-name];
SELECT
name AS UserName,
type_desc AS UserType,
authentication_type_desc AS AuthType,
create_date AS CreatedDate
FROM sys.database_principals
WHERE name = 'your-function-app-name';
π What’s happening here?
- CREATE USER […] FROM EXTERNAL PROVIDER creates a database user linked to the Azure AD managed identity
- ALTER ROLE grants specific permissions (read and write)
- The user name must exactly match the Function App name
- This must be done in the database context, not master!
π¬ Step 3: Create Sample Table While you’re connected, let’s create a sample table to test with:
π¬ Run the query below in the same query editor
CREATE TABLE Users (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Email NVARCHAR(255) NOT NULL UNIQUE,
CreatedDate DATETIME2 DEFAULT GETDATE()
);
CREATE INDEX IX_Users_Email ON Users(Email);
CREATE INDEX IX_Users_CreatedDate ON Users(CreatedDate);
INSERT INTO Users (Name, Email) VALUES
('John Doe', 'john.doe@bartpasmans.tech'),
('Jane Smith', 'jane.smith@bartpasmans.tech'),
('Bob Johnson', 'bob.johnson@bartpasmans.tech'),
('Alice Williams', 'alice.williams@bartpasmans.tech'),
('Charlie Brown', 'charlie.brown@bartpasmans.tech');
SELECT * FROM Users;
Deploy the function code
The ARM template deployed the infrastructure (or you did this step manually), but we need to deploy the function code separately.
π¬ Deploy the Function CodeMethod 1: Via Azure Portal (Quick testing)
- Go to your Function App β Functions β Create
- Choose HTTP trigger
- Name: DatabaseQuery
- Authorization: Function
- Click Create
- Go to Code + Test
- Copy the code below and add it to the function
- Paste and save
using namespace System.Net
param($Request, $TriggerMetadata)
Write-Host "π Database Query Function Started"
Write-Host "π
Timestamp: $(Get-Date)"
Write-Host "π Invocation ID: $($TriggerMetadata.InvocationId)"
$sqlServerName = $env:SqlServerName
$sqlDatabaseName = $env:SqlDatabaseName
Write-Host "ποΈ SQL Server: $sqlServerName"
Write-Host "πΎ Database: $sqlDatabaseName"
$body = @{
success = $false
message = ""
data = $null
timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
}
try {
Write-Host "β
Using built-in System.Data.SqlClient for database connectivity"
Write-Host "π Requesting Azure AD access token for SQL Database..."
$resourceURI = "https://database.windows.net/"
$tokenAuthURI = $env:IDENTITY_ENDPOINT + "?resource=$resourceURI&api-version=2019-08-01"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"X-IDENTITY-HEADER"=$env:IDENTITY_HEADER} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
if ($accessToken) {
Write-Host "β
Access token obtained successfully"
} else {
throw "Failed to obtain access token from managed identity"
}
$action = $Request.Query.action
if (-not $action) {
$action = $Request.Body.action
}
if (-not $action) {
$action = "read"
}
Write-Host "βοΈ Action: $action"
$connectionString = "Server=$sqlServerName; Database=$sqlDatabaseName; Encrypt=True; TrustServerCertificate=False; Connection Timeout=30;"
Write-Host "π Connecting to database using managed identity..."
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.AccessToken = $accessToken
$connection.Open()
Write-Host "β
Database connection established"
$command = $connection.CreateCommand()
switch ($action.ToLower()) {
"read" {
Write-Host "π Executing SELECT query..."
$command.CommandText = "SELECT TOP 10 Id, Name, Email, CreatedDate FROM Users ORDER BY CreatedDate DESC"
$adapter = New-Object System.Data.SqlClient.SqlDataAdapter($command)
$dataSet = New-Object System.Data.DataSet
$adapter.Fill($dataSet) | Out-Null
$results = @()
foreach ($row in $dataSet.Tables[0].Rows) {
$results += @{
Id = $row["Id"]
Name = $row["Name"]
Email = $row["Email"]
CreatedDate = $row["CreatedDate"].ToString("yyyy-MM-dd HH:mm:ss")
}
}
$body.data = $results
$body.message = "Successfully retrieved $($results.Count) records"
Write-Host "β
Retrieved $($results.Count) records"
}
"write" {
$name = $Request.Query.name
$email = $Request.Query.email
if (-not $name) { $name = $Request.Body.name }
if (-not $email) { $email = $Request.Body.email }
if (-not $name -or -not $email) {
throw "Name and email are required for write operation"
}
Write-Host "βοΈ Executing INSERT query..."
Write-Host " Name: $name"
Write-Host " Email: $email"
$command.CommandText = "INSERT INTO Users (Name, Email) VALUES (@Name, @Email); SELECT SCOPE_IDENTITY() AS NewId"
$command.Parameters.AddWithValue("@Name", $name) | Out-Null
$command.Parameters.AddWithValue("@Email", $email) | Out-Null
$newId = $command.ExecuteScalar()
$body.data = @{
Id = [int]$newId
Name = $name
Email = $email
}
$body.message = "Successfully inserted new record with ID: $newId"
Write-Host "β
Inserted new record with ID: $newId"
}
"update" {
$id = $Request.Query.id
$name = $Request.Query.name
$email = $Request.Query.email
if (-not $id) { $id = $Request.Body.id }
if (-not $name) { $name = $Request.Body.name }
if (-not $email) { $email = $Request.Body.email }
if (-not $id) {
throw "ID is required for update operation"
}
Write-Host "π Executing UPDATE query..."
Write-Host " ID: $id"
$updateParts = @()
if ($name) {
$updateParts += "Name = @Name"
$command.Parameters.AddWithValue("@Name", $name) | Out-Null
}
if ($email) {
$updateParts += "Email = @Email"
$command.Parameters.AddWithValue("@Email", $email) | Out-Null
}
if ($updateParts.Count -eq 0) {
throw "At least one field (name or email) must be provided for update"
}
$command.CommandText = "UPDATE Users SET $($updateParts -join ', ') WHERE Id = @Id"
$command.Parameters.AddWithValue("@Id", [int]$id) | Out-Null
$rowsAffected = $command.ExecuteNonQuery()
$body.data = @{
Id = [int]$id
RowsAffected = $rowsAffected
}
$body.message = "Successfully updated $rowsAffected record(s)"
Write-Host "β
Updated $rowsAffected record(s)"
}
"count" {
Write-Host "π Executing COUNT query..."
$command.CommandText = "SELECT COUNT(*) as TotalUsers FROM Users"
$count = $command.ExecuteScalar()
$body.data = @{
TotalUsers = [int]$count
}
$body.message = "Successfully retrieved count"
Write-Host "β
Total users: $count"
}
default {
throw "Invalid action: $action. Supported actions: read, write, update, count"
}
}
$connection.Close()
Write-Host "π Database connection closed"
$body.success = $true
$statusCode = [HttpStatusCode]::OK
} catch {
Write-Error "β Error occurred: $($_.Exception.Message)"
Write-Error "Stack Trace: $($_.Exception.StackTrace)"
$body.success = $false
$body.message = "Error: $($_.Exception.Message)"
$body.error = @{
type = $_.Exception.GetType().FullName
message = $_.Exception.Message
stackTrace = $_.Exception.StackTrace
}
$statusCode = [HttpStatusCode]::InternalServerError
}
Write-Host "π Function execution completed"
Write-Host "βββββββββββββββββββββββββββββββββββββββββββββββββββ"
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $statusCode
Body = $body | ConvertTo-Json -Depth 10
Headers = @{
"Content-Type" = "application/json"
}
})How the Authentication Works
Let’s peek under the hood at what’s happening:
# 1. Function requests Azure AD token for SQL Database
$resourceURI = "https://database.windows.net/"
$tokenAuthURI = $env:IDENTITY_ENDPOINT + "?resource=$resourceURI&api-version=2019-08-01"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"X-IDENTITY-HEADER"=$env:IDENTITY_HEADER} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token
# 2. Create SQL connection without password
$connectionString = "Server=$sqlServerName; Database=$sqlDatabaseName; Encrypt=True;"
$connection = New-Object System.Data.SqlClient.SqlConnection
$connection.ConnectionString = $connectionString
$connection.AccessToken = $accessToken # Token instead of password!
# 3. Open connection and execute queries
$connection.Open()
# Now you can run queries with the granted permissions
π The flow:
- Function starts and requests a token from Azure AD
- Azure AD validates the managed identity and issues a time-limited token
- Function connects to SQL Database using the token
- SQL Server validates the token and checks database permissions
- Query executes with the granted permissions (read/write)
- Connection closes, token expires automatically
No passwords anywhere! π
Let’s Test!
Time to see it in action!π¬ Test in Azure Portal
- Go to your Function App β Functions β DatabaseQuery
- Click Test/Run tab
- Set logs to file system:

- Click Run

- You should see the result:

- Database result:

Summary
That’s a wrap! πIn this blog, we explored how to implement passwordless database authentication using Azure Managed Identity with PowerShell Functions, the secure, serverless way!
We kicked things off by discussing why connection strings are a security risk and how managed identity solves these problems with zero-trust architecture.
Then we rolled up our sleeves and:
β Deployed Azure SQL Server with Azure AD authentication enabled
β Created a Function App with system-assigned managed identity
β Granted granular database permissions (read/write) to the managed identity
β Deployed a PowerShell function that connects using Azure AD tokens
β Tested the solution with multiple database operations
π¬ If you followed along step-by-step, you now have a production-ready, secure, passwordless database access pattern running in Azure.
π This post is all about implementing security best practices without adding complexity. No credentials to manage, automatic token rotation, full audit trail, and granular permissions, all handled by Azure!
Key Takeaways:
- π No passwords stored – Azure AD handles everything
- β‘ Lightweight design – Uses built-in .NET classes
- π Fast cold starts – Optimized for Consumption plan
- π Full audit trail – Every access logged with identity
- π― CORS pre-configured – Test directly in Azure Portal
Thanks again for reading!
I hope this inspires you to implement managed identity in your own projects. Maybe next time you’ll add retry logic, connection pooling, or even integrate with Azure Key Vault for additional secrets!
Let me know what you build! π

