Reputation: 55
We are unable to query a sql database in azure from an Azure App Service when using a user assigned managed identity (it works fine if we use a system assigned managed identity)
The application is a .net core 2.2 web api application.
We have a user assigned identity set up for an Azure App Service.
This identity has been set up as the ad sql admin by using the following command:
az sql server ad-admin create --resource-group iactests --server iactestsql --object-id -u iactestmanagedIdentity
The token is generated like this:
services.AddDbContext<SchoolContext>(options => options.UseSqlServer(new
SqlConnection
{
ConnectionString = configuration.GetConnectionString("SchoolContext"),
AccessToken = isDevelopmentEnvironment ? null : new AzureServiceTokenProvider().GetAccessTokenAsync("https://database.windows.net/").Result
}), ServiceLifetime.Scoped);
This is the error we get:
Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProviderException: Parameters: Connection String: [No connection string specified], Resource: https://database.windows.net/, Authority: . Exception Message: Tried the following 3 methods to get an access token, but none of them worked.
Parameters: Connection String: [No connection string specified], Resource: https://database.windows.net/, Authority: . Exception Message: Tried to get token using Managed Service Identity. Access token could not be acquired. MSI ResponseCode: BadRequest, Response:
Parameters: Connection String: [No connection string specified], Resource: https://database.windows.net/, Authority: . Exception Message: Tried to get token using Visual Studio. Access token could not be acquired. Visual Studio Token provider file not found at "D:\local\LocalAppData\.IdentityService\AzureServiceAuth\tokenprovider.json"
Parameters: Connection String: [No connection string specified], Resource: https://database.windows.net/, Authority: . Exception Message: Tried to get token using Azure CLI. Access token could not be acquired. 'az' is not recognized as an internal or external command,
operable program or batch file.
at Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProvider.GetAuthResultAsyncImpl(String authority, String resource, String scope)
at Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProvider.GetAuthenticationResultAsync(String resource, String tenantId)
at Microsoft.Azure.Services.AppAuthentication.AzureServiceTokenProvider.GetAccessTokenAsync(String resource, String tenantId)
--- End of inner exception stack trace ---
If we use a system assign identity and configure the sql ad admin to be said identity, it works fine
Any ideas?
Thanks in advance
Upvotes: 2
Views: 3290
Reputation: 96
The AppAuthentication library now supports specifying user-assigned identities for Azure VMs and App Services as of the 1.2.0-preview2 release.
To use a user-assigned identity, you will need to set an AppAuthentication connection string of the format:
RunAs=App;AppId={ClientId of user-assigned identity}
The AppAuthentication connection string can be set as an argument passed to the AzureServiceTokenProvider constructor or specified in the AzureServicesAuthConnectionString environment variable. For more information on AppAuthentication connection strings, see here.
Upvotes: 4
Reputation: 89141
It looks like AzureServiceTokenProvider does not support user assigned managed identities, at least at this point. AzureServiceTokenProvder is a wrapper over the local HTTP endpoint that provides tokens to the application.
I was looking into this, and appears that you must provide the clientId of the user assigned managed identity to the endpoint to get a token. And AzureServiceTokenProvider doesn't have a way to do that (at least that I could figure out).
User assigned managed identities adds the ability to have multiple User assigned managed identities for an application. So the API to get a token needs to specify which MSI you want, the system-assigned MSI, or one of the user-assigned MSIs. The way the HTTP endpoint does this is that it uses the system-assigned MSI unless you specify a clientId.
In any case, you can hit the token endpoint directly, and provide the clientId of the user-assigned MSI like this:
public async Task<String> GetToken(string resource, string clientId = null)
{
var endpoint = System.Environment.GetEnvironmentVariable("MSI_ENDPOINT", EnvironmentVariableTarget.Process);
var secret = System.Environment.GetEnvironmentVariable("MSI_SECRET", EnvironmentVariableTarget.Process);
if (string.IsNullOrEmpty(endpoint))
{
throw new InvalidOperationException("MSI_ENDPOINT environment variable not set");
}
if (string.IsNullOrEmpty(secret))
{
throw new InvalidOperationException("MSI_SECRET environment variable not set");
}
Uri uri;
if (clientId == null)
{
uri = new Uri($"{endpoint}?resource={resource}&api-version=2017-09-01");
}
else
{
uri = new Uri($"{endpoint}?resource={resource}&api-version=2017-09-01&clientid={clientId}");
}
// get token from MSI
var tokenRequest = new HttpRequestMessage()
{
RequestUri = uri,
Method = HttpMethod.Get
};
tokenRequest.Headers.Add("secret", secret);
var httpClient = new HttpClient();
var response = await httpClient.SendAsync(tokenRequest);
var body = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(body);
string token = result["access_token"].ToString();
return token;
}
Upvotes: 2