Azure AD Service Principal authentication to SQL DB - Code Sample
Published Apr 23 2019 03:40 PM 78.5K Views
Microsoft

The following application provides an example of using Azure AD Service Principal (SP) to authenticate and connect to Azure SQL database. This application measures the time it takes to obtain an access token, total time it takes to establish a connection, and time it takes to run a query. The application can be used to troubleshoot delays during each phase of the connection and query process. In addition, this code sample can display the content of the access token obtained using Azure AD SP authentication. Please note that the same authentication mechanism applies to Managed Instance and SQL DW.

For more information on how to enable Azure AD authentication for SQL DB see  
https://azure.microsoft.com/en-us/documentation/articles/sql-database-aad-authentication/


Before building and running the code sample, perform the following steps:

  1. Create a Service Principal in Azure AD for your service and obtained the following information required to execute the code sample below
    a.  Application ID of the Service Principal (SP)
        clientId = "<appId>"; // Application ID of the SP
        (e.g. string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";)
    b. Copy the “Display Name” of your application which will be used in step 3)
       (e.g.”debugapp” as a “Display Name” for the app above)
    c.  Azure AD tenant ID
         aadTenantId = "<tenantId>"; //Azure AD tenant ID
         (e.g. string aadTenantId = "xxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx";)
    d.  Client (SP) secret key
         clientSecretKey = "secretKey"; // Application secret key 
         (e.g. string clientSecretKey = "xxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxx/xxx";)
    To obtain above information follow the steps indicated in the link below
    https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-porta...
  2. Find the Azure SQL Server name and Database name
    serverName = "<serverName>"; // server name: myserver.database.windows.net
    (e.g. string serverName = "myserver.database.windows.net";)
    databaseName = "<databaseName>" // database name: test;
    (e.g. string databaseName = "test";)
  3. Using SSMS to connect to SQL DB (e.g. “test”) as an Azure AD user with proper Azure AD permissions (e.g. Azure AD admin for SQL DB), create an application user from step 1 above. Execute the T-SQL statement create user command “create user [app display name] from external provider”.
    Example using “debugapp” as a display name form step1
        create user [debugapp] from external provider.
    Note that the create user command grants this user a connect permission to the database, which is sufficient enough to execute the sample program below.
  4. Copy and execute the program indicated below

Below is an example of the program output.

debugg.png

 

 

Please note that the token information displaying the access token was commented out in the program output
//Display a token

//Console.WriteLine("This is your token: " + authenticationResult.AccessToken);

If a token display is enabled (as it is in the program below), it can be copied and decoded into a readable form with claims, using https://jwt.ms/

.

Below is a C# version of the application called Program.cs

To obtain the nuget package “Microsoft.IdentityModel.Clients.ActiveDirectory”, use the link below

https://www.nuget.org/api/v2/package/Microsoft.IdentityModel.Clients.ActiveDirectory/4.5.1.

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AADTest
{
    class Program
    {
        static void Main(string[] args)
        {
         
            // Examples for the input parameters
            // string serverName = "<serverName>"; // server name i.e. sqlxx.database.windows.net
            // string databaseName = "<databaseName>"; //Database name  i.e. test 
            // string clientId = "<appId>"; // application id of the service principal
            // string aadTenantId = "<tenantId>"; //AAD tenant id
            // string clientSecretKey = "secretKey"; // AAD app secret key

            string serverName = "myserver.database.windows.net"; 
            string databaseName = "test";
            string clientId = "xxxxxx-xxxxx-xxxxx-xxxx-xxxx"; 
            string aadTenantId = "xxxxxx-xxxxxx-xxxxxx-xxxxxx-xxxxxxxx";
            string clientSecretKey = "xxxxx/xxxxxx/xxxxx";

            string sqlConnectionString = String.Format("Data Source=tcp:{0},1433;Initial Catalog={1};Persist Security Info=False;Connect Timeout=30;Encrypt=True;TrustServerCertificate=False", serverName, databaseName);

            string AadInstance = "https://login.windows.net/{0}";
            string ResourceId = "https://database.windows.net/";


            AuthenticationContext authenticationContext = new AuthenticationContext(string.Format(AadInstance, aadTenantId));
            ClientCredential clientCredential = new ClientCredential(clientId, clientSecretKey);

            DateTime startTime = DateTime.Now;
            Console.WriteLine("Time " + String.Format("{0:mm:ss.fff}", startTime));

            AuthenticationResult authenticationResult = authenticationContext.AcquireTokenAsync(ResourceId, clientCredential).Result;

            DateTime endTime = DateTime.Now;
            Console.WriteLine("Got token at " + String.Format("{0:mm:ss.fff}", endTime));

            Console.WriteLine("Total time to get token in milliseconds " + (endTime - startTime).TotalMilliseconds);

            using (var conn = new SqlConnection(sqlConnectionString))
            {
                conn.AccessToken = authenticationResult.AccessToken;

                startTime = DateTime.Now;
                Console.WriteLine("Starting to open connection at " + String.Format("{0:mm:ss.fff}", startTime));

                //Display a token
                Console.WriteLine("This is your token: " + authenticationResult.AccessToken);

                conn.Open();

                endTime = DateTime.Now;
                Console.WriteLine("Got connection at " + String.Format("{0:mm:ss.fff}", endTime));

                Console.WriteLine("Total time to establish connection in milliseconds " + (endTime - startTime).TotalMilliseconds);

                startTime = DateTime.Now;
                Console.WriteLine("Starting to run query at " + String.Format("{0:mm:ss.fff}", startTime));

                using (var cmd = new SqlCommand("SELECT 1", conn))
                {
                    var result = cmd.ExecuteScalar();
                    Console.WriteLine(result.ToString());
                }

                endTime = DateTime.Now;
                Console.WriteLine("Completing running query at " + String.Format("{0:mm:ss.fff}", endTime));
                Console.WriteLine("Total time to execute query in milliseconds " + (endTime - startTime).TotalMilliseconds);
            }

            Console.ReadKey();
        }
    }
}

 

19 Comments
Copper Contributor

How to login to the test database using the SSMS and not through code? Is this even supported? If yes, What would be the option to choose for authentication to sign in and is the secret key the password? 

Copper Contributor

@jnprakash You can not login to the Azure SQL with SSMS using the service principal as I know 

https://dba.stackexchange.com/questions/184598/unable-to-connect-using-azure-ad-service-principal-on...

Copper Contributor

@Mirek Sztajno Very good tutorial, but is there a way to run 3 step automatically in CI/CD? with Powershell or Azure Devops or somehow else but automatically = without SSMS?

Microsoft

@jnprakash At this point we do not have this option in our client tools like SSMS. I will update once we have added this support. 

Microsoft

@R-Chaser , there are some possibilities, depending on your scenario. Can you please email us at  SQLAADAuth@microsoft.com and we can discuss the options with you. 

Copper Contributor

@AmolAgarwalSQL I have the same issue as @R-Chaser and already wrote to the mailing address you mentioned but I haven't got any response.

 

Could you share some feedback here how to automate this process in Azure Pipelines when it is NOT possible to create external (AD) SQL users when signed in as Service Principal?

Microsoft

Please contact SQLAADFeedback@microsoft.com alias to discuss the details 

Microsoft

@Mirek Sztajno  can you please reach out to @R-Chaser and discuss options with him. 

Microsoft

Use this when there is need to get token for user. This will be interactive though. 

From PowerShell

Install-Module -Name MSAL.PS

 

restart powershell.

 

$authority = "https://login.microsoftonline.com/common"
#this is the security and compliance center endpoint
$resourceUrl = "https://database.windows.net/"
$clientId = "1950a258-227b-4e31-a9cf-717495945fc2" # known powershell client id

$redirectUri = "urn:ietf:wg:oauth:2.0:oob"

Get-MsalToken -ClientId $clientId -RedirectUri $redirectUri
$response.AccessToken | clip

 

From C# program

string clientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // known powershell client id
string aadTenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";

string ResourceId = "https://database.windows.net/";
string AadInstance = "https://login.windows.net/{0}";

AuthenticationContext authenticationContext = new AuthenticationContext(string.Format(AadInstance, aadTenantId));

var user = new UserCredential("myalias");

AuthenticationResult authResult = authenticationContext.AcquireTokenAsync(ResourceId, clientId, user).Result

 

 

(Edit: client IDs and tenant ID removed from code example /cc @AmolAgarwalSQL)

Microsoft

When we deployed the production, there is no client secret key anymore.  How will the example work without a client secret key?  Do we just leave that blank?

 

<Amol> This will not work without a secret key, how is your app getting a token, Once you have a token, you can directly put it here  and then connect. Token will start with something like ey....

conn.AccessToken = "<your token>";

 

Copper Contributor

What is the approach for handling expiration of token? We have a spring boot app in our AKS and it's restarting every hour because the AAD token to connect to AZURE SQL expires every hour.

Microsoft

@MarcusDNguyen , if token is expired, client has to get a new token from AAD and connect. So, a retry logic is needed which can detect that token has expired and gets a new token. 

Below is the link from AAD on token lifetime. 

https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview#:~:text=Access%20%26%20I...

Copper Contributor

It would be great if tools like SQLPackage or Invoke-SqlCmd could handle Service Principals as credentials (key or certificate), especially for deployment scenarios in Azure DevOps

 

As of today, the only way I have found to do this is deploying all that is not related to AAD using the DacPac file under an SQL account and after then create users using this kind of trick.

 

Maybe I am missing something but it is an important feature that is absent

Copper Contributor

@odelmotte You can use plain SqlConnection/SqlCommand to run SQL script with service principal, however in such case you can only use 'normal' SQL syntax, and you cannot have 'GO' keyword in your script since it is a special syntax that only SQLCMD understands. If you need to use 'GO' keyword, you can still use SQL Server Management classes in Powershell. The example below should help (you of course need to provide all required variables). Note we use some Azure CLI commands there for simplicity.

 

Import-Module -Name SqlServer

# Use Azure CLI to get token, much easier than using Powershell for that
az login --service-principal -u $AppId -p $Secret --tenant $TenantId --allow-no-subscriptions | Out-Null
$accessToken = az account get-access-token --resource https://database.windows.net/ --query "accessToken" --output tsv
$connectionString = "Server=tcp:$ServerName,1433;Initial Catalog=$DatabaseName;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;" 

try {
   # Create the connection object 
   $connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
   $connection.AccessToken = $accessToken

   # Opens connection to Azure SQL Database and return connection
   $connection.Open() 

   # Running query with SQL SMO library rather than plain SqlCommand because our scripts contain
   # multiple batches separated with 'GO' keyword, which is not allowed normally in SqlCommand. More info:
   # https://smehrozalam.wordpress.com/2009/05/12/c-executing-batch-t-sql-scripts-with-go-statements/
   $svrConnection = New-Object Microsoft.SqlServer.Management.Common.ServerConnection($connection)
   $server = New-Object Microsoft.SqlServer.Management.Smo.Server($svrConnection)
   $server.ConnectionContext.ExecuteNonQuery($CmdText)
   $connection.Close()
} catch {
   Write-Error $_.Exception.ToString()
   throw
}
Copper Contributor

Yes, this an option for scripts but what about manipulating DacPac files

 

I have been working on many Azure Pipelines and I am stuck with SQL credentials to do the deployment part and the security is handled using something else.

 

When deploying to Azure, I would like to rely on the ARM connection I provide to the task to handle the login and then the deployment of the DacPac with my AAD related stuff.

Microsoft

@odelmotte please shoot an email to SQLAADFeedback@microsoft.com with all the details and scenarios. As needed, we will involve more teams. 

Copper Contributor

@AmolAgarwalSQL will do next week. thank you

Copper Contributor

Hi, have pushed a PR on Azure Pipelines Tasks to handle SP connections : https://github.com/microsoft/azure-pipelines-tasks/pull/13770 

Working on making the CI pass

Copper Contributor

You can use the sql powershell scripts for CRUD operations in Azure SQL tables. Modify the scripts according to the operation you perform. Below is a sample code

$sqlcmd = New-Object System.Data.SqlClient.SqlCommand

$sqlcmd.Connection = $sqlConn

$sqlcmd.CommandText = "INSERT into parametermaster(paramid,ParamGroup,Value) VALUES (@paramid, @ParamGroup, @Val)"

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@paramid",[Data.SQLDBType]::Int)))

$sqlcmd.Parameters["@paramid"].Value = $paraID

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@ParamGroup",[Data.SQLDBType]::VarChar, 50)))

$sqlcmd.Parameters["@ParamGroup"].Value = $ParaGrp

$sqlcmd.Parameters.Add((New-Object Data.SqlClient.SqlParameter("@val",[Data.SQLDBType]::NVarChar, 500)))

$sqlcmd.Parameters["@val"].Value =$ParaValue

$sqlcmd.ExecuteNonQuery();

 

Full script can details available in this blog

https://www.sharepointcollabs.com/2021/09/update-azure-sql-database-table-using.html

 

Version history
Last update:
‎Nov 09 2020 09:42 AM
Updated by: