Using Active Directory Service Principal authentication to connect to Azure SQL

This article describes how to authorize a .NET application to connect to an Azure SQL Server through Azure Active Directory.

Why

You don't want to connect to a database with your personal account details. You want to be able to control the login over time, expiring access as needed.

Register your application

Presuming you have an Azure SQL Server and a Database you'd like to connect to, you'll need to register your application in Azure AD. This is done through the App Registrations blade:


You won't need to register a Redirect URL for this example.

At this point, it will be prudent to write down the following properties for later use:


  • Application (client) ID (called AppId in the accompanying code)
  • Directory (tenant) ID (called TenantId in the accompanying code)

Grant access to Azure SQL Database

Next up, we need to give your application access to the Azure SQL Database service. This is done through the API Permissions blade:






Add client authentication

In order for your application to be able to authenticate as this Active Directory representation of it, switch over to Certificates & secrets and create a New client secret:




Write down the secret Value for later use (called AppSecret in the accompanying code).


Configure the Azure SQL

Switch over to your Azure SQL Server instance and select the Active Directory admin blade, ensuring you select an appropriate account (our managed application in this example):


Then, head over to Firewalls and virtual networks, ensuring you accept connections from the machine where your .NET application is running:


Write down
  • your instance name (testingmsaccess in this example; called DatabaseServer in the accompanying code)
  • your database name (called DatabaseInstance in the accompanying code)
  • and the resource group the server lives in (available from the Overview pane; called ResourceGroupName in the accompanying code)


Getting access to the database

At this point, your .NET application is able to connect to the database server, but cannot yet access any database. To go the last mile, we unfortunately have to run some PowerShell ... 

Pre-requisites 

Run Windows PowerShell 5.1 as Administrator



Access continued

OK, you can close the admin version now and start a standard user copy.


# Connect to Azure Resource Manager (notice the new login window with ALT+TAB)
Connect-AzureRmAccount -Tenant $TenantId

# Optional: Select the appropriate subscription
Select-AzureRmSubscription -Subscription "Visual Studio Enterprise"

# Assign an identity to the Azure SQL logical server
Set-AzureRmSqlServer `
    -ResourceGroupName $ResourceGroupName `
    -ServerName $DatabaseServer `
    -AssignIdentity

# Assign Directory Readers permission to the SQL logical server identity
Load-Module AzureAD

# Connect to Azure Active Directory (notice the new login window with ALT+TAB)
Connect-AzureAD -TenantId $TenantId

## Get Azure AD role "Directory Users" and create if it doesn't exist
$RoleName = "Directory Readers"
$role = Get-AzureADDirectoryRole | Where-Object {$_.displayName -eq $roleName}
if ($null -eq $role) {
    # Instantiate an instance of the role template
    $roleTemplate = Get-AzureADDirectoryRoleTemplate |
        Where-Object {$_.displayName -eq $roleName}
    Enable-AzureADDirectoryRole `
        -RoleTemplateId $roleTemplate.ObjectId
    $role = Get-AzureADDirectoryRole | 
        Where-Object {$_.displayName -eq $roleName}
}

## Get service principal for managed instance
$roleMember = Get-AzureADServicePrincipal -SearchString $DatabaseServer 
$roleMember.Count
if ($null -eq $roleMember) {
    Write-Output "Error: No Service Principals with name '$($DatabaseServer)', `
make sure that AssignIdentityName parameter was entered correctly."
    exit
}

if (-not ($roleMember.Count -eq 1)) {
    Write-Output "Error: More than one service principal with name pattern `
'$($DatabaseServer)'"
    Write-Output "Dumping selected service principals...."
    $roleMember
    exit
}

## Check if service principal is already member of readers role
$allDirectoryReaders = Get-AzureADDirectoryRoleMember -ObjectId $role.ObjectId
$selectedDirectoryReader = $allDirectoryReaders |
    Where-Object { $_.ObjectId -match $roleMember.ObjectId}
 
if ($null -eq $selectedDirectoryReader) {
    Write-Output "Adding service principal '$($DatabaseServer)' to `
'Directory Readers' role'..."
    
    Add-AzureADDirectoryRoleMember `
        -ObjectId $role.ObjectId `
        -RefObjectId $roleMember.ObjectId
    
    Write-Output "'$($DatabaseServer)' service principal added to `
'Directory Readers' role'..."
else {
    Write-Output "Service principal '$($DatabaseServer)' is already`
a member of 'Directory Readers' role'."
}

# Get Access Token
Import-Module MSAL.PS
$result = Get-MsalToken `
    -ClientId $AppId `
    -ClientSecret (ConvertTo-SecureString $AppSecret -AsPlainText -Force) `
    -TenantId $TenantId `
    -Scopes "https://database.windows.net/.default"
$AccessToken = $result.AccessToken


Notice the Scopes above. This represents what we are trying to access - Microsoft Azure's database service. We'll refer back to this in our .NET application.

We are now ready to connect to our database and create a user association with our Active Directory Service Principal:

$conn = New-Object System.Data.SqlClient.SQLConnection 
$conn.ConnectionString = "Data Source=$DatabaseServer.database.windows.net;`
Initial Catalog=$DatabaseName;Connect Timeout=30"
$conn.AccessToken = $AccessToken

$conn.Open()

$statement = "CREATE USER [$AppRegistrationName] FROM EXTERNAL PROVIDER;"
$command = New-Object -TypeName System.Data.SqlClient.SqlCommand($statement$conn)       
$command.ExecuteNonQuery()

$statement = "ALTER ROLE db_owner add member [$AppRegistrationName];"
$command = New-Object -TypeName System.Data.SqlClient.SqlCommand($statement$conn)
$command.ExecuteNonQuery()

$conn.Close()

Done! No more PowerShell!


Connecting from our .NET application

With all of the above set up in place (surely there must be an easier way ... ?), we are now ready to make a connection from our .NET application.

You'll need to reference the NuGet package Microsoft.Identity.Client (I used 4.3.10).

We start off with a brief connection string:

readonly string _connectionString = $@"Server={DatabaseServer}; Database={DatabaseInstance}";

Then, we set up our Azure Active Directory application representation:

IConfidentialClientApplication app = ConfidentialClientApplicationBuilder
    .Create(AppId)
    .WithClientSecret(AppSecret)
    .WithAuthority($"https://login.microsoftonline.com/{TenantId}")
    .Build();

Time to get our access token:

var token = await app
    .AcquireTokenForClient(new List<string>
    {
        "https://database.windows.net/.default"
    })
    .ExecuteAsync();

... before we can finally connect to our database:

await using var conn = new SqlConnection(_connectionString);
conn.AccessToken = token.AccessToken;
conn.Open();

And that's all (‼) that is required to connect to Azure SQL using an Active Directory Service Principal!

Comments

Popular posts from this blog

Auto Mapper and Record Types - will they blend?

Unit testing your Azure functions - part 2: Queues and Blobs

Testing WCF services with user credentials and binary endpoints