Azure.Messaging.ServiceBus Create a ServiceBusClient using a System Assigned Managed Identity - azure

I'm migrating a servicebus client application from Microsoft.Azure.ServiceBus to use the current library Azure.Messaging.ServiceBus.
The application is a Worker Process running on a virtual machine in windows azure.
The VM has a system assigned managed identity which grants it access to service bus and we have been using it successfully with the old library for over a year.
On the old library we created a client using this connection string
Endpoint=sb://MyNamespace.servicebus.windows.net/;Authentication=Managed Identity
When I put that connection string into the constructor of Azure.Messaging.ServiceBus.ServiceBusClient I get the following error
The connection string used for an Service Bus client must specify the Service Bus namespace host and either a Shared Access Key (both the name and value) OR a Shared Access Signature to be valid. (Parameter 'connectionString')
I've been trawling through documents for some time now with no progress.
Is there anyway to make this work?
Ideally I would continue to use the connection string - developer machines do not have system assigned ID's so we develop with key based connection strings and let devops swap in the correct prod connection string.
UPDATE
Following on from Jesse's answer managed identity has to go trough a separate constructor which requires a namespace instead of an endpoint and an instance of ManagedIdentityCredential.
As I mentioned not all environments where we deploy have managed aged identities, some require a SharedAccessKey based connection string.
Instead introducing new "identity type" configuration parameters into our build process I've used a factory method to parse the connection string and call the correct constructor overload. Where its a managed identity It extracts the namespace from the endpoint setting.
I Hope its useful for others
private static ServiceBusClient CreateServiceBusClient(string connectionString)
{
var cs = new DbConnectionStringBuilder();
cs.ConnectionString = connectionString;
if (cs.ContainsKey("Authentication") &&
"Managed Identity".Equals(cs["Authentication"].ToString(), StringComparison.OrdinalIgnoreCase))
{
string endpoint = cs["Endpoint"].ToString() ?? String.Empty;
if (endpoint.StartsWith(#"sb://", StringComparison.OrdinalIgnoreCase)) endpoint = endpoint.Substring(5);
if (endpoint.EndsWith(#"/")) endpoint = endpoint.Substring(0, endpoint.Length - 1);
return new ServiceBusClient(endpoint, new ManagedIdentityCredential());
}
return new ServiceBusClient(connectionString);
}
it needs the Azure.Identity package and the namespace System.Data.Common for the connection string builder.

The clients in the Azure.Messaging.ServiceBus package support connection strings only in the format that the Azure portal returns them.
The ;Authentication=Managed Identity token that you've included in your connection string is not a known token and is ignored, so the client does not have the information needed to perform authorization. A managed identity cannot be specified via connection string.
To use a managed identity, you'll use one of the constructor overloads that accepts a fully qualified namespace and a TokenCredential. An example can be found in the package Overview docs. Any of the Azure.Identity credentials can be used; you may want to take take a look at the managed identity section of the Azure.Identity overview.

Related

Azure Service Bus: The connection string used for an Service Bus client must specify the Service Bus namespace host and either a Shared Access Key

Why is my RootManageSharedAccessKey Connection String Invalid?
Code:
When executing the code below, I receive an error about an invalid connection string:
serviceBusClient <- new ServiceBusClient(connectionString)
Error:
System.NotImplementedException: 'The method or operation is not
implemented.'
System.ArgumentException: 'The connection string used for an Service
Bus client must specify the Service Bus namespace host and either a
Shared Access Key (both the name and value) OR a Shared Access
Signature to be valid. Parameter name: connectionString'
ConnectionString:
"Endpoint=sb://myNameSpace.servicebus.windows.net/;TransportType=AmqpWebSockets;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=*******************************************="
I also tried removing the TransportType property from the connection string:
"Endpoint=sb://myNameSpace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=*******************************************="
The connection string was derived from the Shared Access Policy of my Service Bus resource:
Context:
Xamarin.Forms (Android)
Azure.Messaging.ServiceBus
NuGet Packages:
Azure.Messaging.ServiceBus (7.11.1)
Xamarin.Forms (5.0.0.2545)
For those who may be interested, the root cause of this turned out to be that the connection string passed to ServiceBusClient was JSON-encoded, causing two quote literals to be embedded. As a result, there was no Endpoint= token present (it was "Endpoint=) which left the client unable to determine where to connect to.

How to read connection string from key vault for Service Bus?

Here is how I instantiate the client in my Configure method:
services.AddSingleton<ServiceBusClient>(x => new ServiceBusClient(configuration.GetSection("ServiceBus:ConnectionString").Value, serviceBusClientOptions));
And this how my appsettings looks like:
{
"ServiceBus:ConnectionString": "#Microsoft.KeyVault(VaultName=MyVaultName;SecretName=MySecretName)"
}
However, I am getting the following exception:
The connection string used for an Service Bus client must specify the Service Bus namespace host and either a Shared Access Key (both the name and value) OR a Shared Access Signature to be valid. (Parameter 'connectionString'
What am I missing here?
Have you created a managed identity for you application and added access policies such that your app can GET this secret value from key vault?
Check out the official documentaion for this here : https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references
Also on a side note, have you tried directly adding the secret value as the appsetting value instead of referencing it from KV and see if that worked? (if yes then definitely its a permissions issue and NOT a problem with your C# app code.

Azure Identity: Trying to get GetUserDelegationKey to work with an application Service Principal

It's not a good sign when the method I'm asking about, GetUserDelegationKey, yields zero search results on SO. Good luck, me.
I have a C# console app, .Net framework 4.8, using Azure.Storage.Blobs and Azure.Identity that will run on customer servers and access Azure blob storage to hold some stuff. I'm doing all of this with the library, not rolling my own REST. Built with VS2019, testing on Win10.
The plan is to use a single Azure storage account that I own, and create one Container per customer project with per-customer credentials that permit them only their own container. Projects never ever talk to each other.
I could set up credentials in the Azure portal by hand, but I am stubbornly trying to do this in software, where a simple project-management app connects as the project app's service principal (which I defined in Azure AD), creates the container, then creates the shared access signatures with a limited lifetime.
The storage account name / container name / access signature would then be configured on the customer server.
I'm having a terrible time.
Note: this is using the newer BlobClient mechanisms, not the older CloudBlob stuff. Dunno if that matters.
This is all documented here at Microsoft, and following even the simple example gets me the same failure.
using System;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Azure.Identity;
namespace Azure.Test
{
class Program
{
static void Main(string[] args)
{
var serviceClient = new BlobServiceClient(
new Uri("https://stevestorageacct.blob.core.windows.net"),
new DefaultAzureCredential(true)); // true=pop up login dlg
/*BOOM*/ UserDelegationKey key = serviceClient.GetUserDelegationKey(
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow.AddDays(30));
// use the key to create the signatures
}
}
}
Even though this program couldn't be simpler, it fails every time with an XML error calling GetUserDelegationKey
Unhandled Exception: Azure.RequestFailedException: The value for one of the XML nodes is not in the correct format.
RequestId:c9b7d324-401e-0127-4a4c-1fe6ce000000
Time:2020-05-01T00:06:21.3544489Z
Status: 400 (The value for one of the XML nodes is not in the correct format.)
ErrorCode: InvalidXmlNodeValue
The XML being sent is supposed to be super simple, I think just the start/end dates for validity, but I have no idea how to get to it to inspect, and http is forbidden for this kind of call, so no Wireshark.
It also fails the same way when I use my application's service principal:
static void Main(string[] args)
{
var tokenCredential = new ClientSecretCredential(
"xxxx-xxxx-xxxx-xxxxx", // tenant ID
"yyyy-yyyy-yyyy-yyyyy, // application ID
"**************"); // client secret
var serviceClient = new BlobServiceClient(
new Uri("https://stevestorageacct.blob.core.windows.net"),
tokenCredential);
UserDelegationKey key = serviceClient.GetUserDelegationKey(
DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow.AddDays(30));
// ALSO: boom
I'm really at a loss.
I suppose I could try rolling my own REST and playing with it that way, but it doesn't feel like this should be necessary: this kind of error feels like a bug even if I'm doing something wrong. XML nodes?
Also open to entirely different ways of approaching this problem if they are superior, but would like to at least find out why this is failing.
I've had some issues with this also. The first things to try is removing the start time (pass null) or setting it ~15 minutes in the past. This is to avoid clock skew between the requesting pc and azure servers.
The second thing to verify is that the user that you are using has the "Storage Blob Data Contributor" role on the storage account. I had to grant it at the storage account level in the end otherwise it just refused to work for me. However in your use case it might be that you need to grant it at the container level to allow you to have one container per client.
Hope this helps.

Log4net ADOAppender use Azure MSI connection instead of plain connection string

Hi I posted this to the log4net user group but thought I'd post it here as well.
I'm working on a project that requires Azure MSI to connection from Azure PaaS to Azure SQL. Wondering if log4net’s ADOAppender supports this already connection mechanism already. From what I can tell it doesn't but thought I'd ask the community before extending log4net.
To support MSI apps can’t connect to a database with a connectionstring alone, they need to get an access token from Azure and then set the AccessToken property on the SqlConnection object. Like the code below is doing:
private static SqlConnection GetSqlConnection()
{
var sqlConnection = new SqlConnection(GetConnectionString());
if (sqlConnection.DataSource != "(localdb)\\MSSQLLocalDB")
sqlConnection.AccessToken = new AzureServiceTokenProvider()
.GetAccessTokenAsync("https://database.windows.net/").Result;
return sqlConnection;
}
This code is using two Microsoft nuget packages to get the access token.
Thanks!

Azure .NET SDK - List all virtual machines, failed to authenticate

Using the new Windows Azure SDK for .NET, I want to get a list of all virtual machines.
Piecing together the early documentation, here's what I came up with:
// This is a custom cert I made locally. I uploaded it to Azure's Management Certificates and added it to my local computer's cert store, see screenshot below.
X509Certificate2 myCustomCert = await this.GetAzureCert();
var credentials = new CertificateCloudCredentials(mySubscriptionId, myCustomCert);
using (var cloudServiceClient = CloudContext.Clients.CreateCloudServiceManagementClient(credentials))
{
credentials.InitializeServiceClient(cloudServiceClient); // Is this required? The next call fails either way.
// This line fails with exception: "The server failed to authenticate the request. Verify that the certificate is valid and is associated with this subscription."
var services = await cloudServiceClient.CloudServices.ListAsync(CancellationToken.None);
}
My first thought was the cert was bad, but I am able to successfully call the Azure REST API using my custom certificate. As you can see below, it is properly added to the Azure Management Certificates and associated with my subscription:
What am I doing wrong?
Here's another option - rather than upload a cert, try pulling your management cert out of your publishsettings file and using the X509Certificate's constructor that takes a byte[]. Then, pass that parameter the result of a call to Convert.FromBase64String, passing it the string representation of your management certificate from your publishsettings file.
Also, take a look at the Compute management client rather than the Cloud Service Management client. There are more features specific to the compute stack in that client at this time. The code below is a demonstration of such an approach. Note, my _subscriptionId and _managementCert fields are both strings, and I just hard-code them to the values from my publishsettings file as I described above.
public async static void ListVms()
{
using (var client = new ComputeManagementClient(
new CertificateCloudCredentials(_subscriptionId,
new X509Certificate2(Convert.FromBase64String(_managementCert)))
))
{
var result = await client.HostedServices.ListAsync();
result.ToList().ForEach(x => Console.WriteLine(x.ServiceName));
}
}
There's a parameterless ListAsync method that's an extension method. Try importing the Microsoft.WindowsAzure.Management namespace (or the Microsoft.WindowsAzure.Management.Compute namespace). Once you see the parameterless ListAsync method you should be good. I'll also mock up some code to resemble what you're trying to accomplish and offer up a more comprehensive answer by the end of the day.

Resources