2
votes

How can I use the Azure SDK to create a new Azure SQL Server within a resource group from a daemon (technically a queue processor)? The problem I'm encountering is an authorization error when attempting to do the create. Here's the exception:

Hyak.Common.CloudException AuthorizationFailed: The client 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' with object id 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' does not have authorization to perform action 'Microsoft.Sql/servers/write' over scope '/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/{myResourceGroupName}/providers/Microsoft.Sql/servers/{newServerName}'.

(The actual exception has real values in each of the placeholders.)

I'm using the Microsoft.Azure.Management.Sql NuGet package, version 0.38.0-prerelease (latest currently available).

I previously attempted to use CertificateCloudCredentials to make the change -- however it yielded the following exception:

Hyak.Common.CloudException AuthenticationFailed: Authentication failed. The 'Authorization' header is not present or provided in an invalid format.

Now I'm using TokenCloudCredentials, obtained in the following way:

AuthenticationContext aContext = new AuthenticationContext(string.Format(
            anApiConfiguration.LoginUriFormat,
            anApiConfiguration.TenantId));
ClientCredential aClientCredential = new ClientCredential(
            anApiConfiguration.ClientId, 
            anApiConfiguration.ClientSecretKey);
AuthenticationResult aTokenResponse = await aContext.AcquireTokenAsync(
            anApiConfiguration.BaseResourceUrl, 
            aClientCredential);

From this I successfully obtain a token. I then use that token as follows:

SqlManagementClient sql = new SqlManagementClient(anAuthToken);
ServerCreateOrUpdateProperties someProperties = 
    new ServerCreateOrUpdateProperties {
        AdministratorLogin = someSettings.AdminAccountName,
        AdministratorLoginPassword = someSettings.AdminPassword,
        Version = "12.0"
};
ServerGetResponse aCreateResponse = 
    await sql.Servers.CreateOrUpdateAsync(
        someSettings.GetResourceGroup(aRegionType),
        aServerName, 
        new ServerCreateOrUpdateParameters(someProperties, aRegion));

That final line, the CreateOrUpdateAsync call, is what yields the exception at the top of this post.

Within Azure Active Directory the client application is granted the Access Azure Service Management (preview) delegated permission (the only one permission which exists for that application) within the Windows Azure Service Management API application. (If it matters, I've also granted the client application all available permissions for the Windows Azure Active Directory application.)

2
Where is your daemon running?Shaun Luttin
Why are you not using this package Microsoft.WindowsAzure.Management.Sql?Shaun Luttin
Right now the code above is being manually executed from an integration test. I just added that detail to indicate that this will be run from a web job, processing requests from an Azure queue, once deployed.Gnosian
I'm not using Microsoft.WindowsAzure.Management.Sql because it does not support creating the server within a resource group.Gnosian
Where did you find Microsoft.Azure.Management.Sql? It is not in my NuGet feed. No worries. Found it with -IncludePrerelease flag.Shaun Luttin

2 Answers

5
votes

I think I have figured out what the problem is and how to fix it (I was also able to reproduce this on my work computer).

Cause

It looks like in the sample app the default method for getting the authentication token is using the wrong account. For example my Subscription and Application are under my example@outlook.com email account, but when I run the console application from my work computer the application gets a token for username@company.com and tries to perform the actions under that account. It seems like the code for getting the authentication token is using some cached account information from the computer (IE?).

The error message you see relates to RBAC (Role-Based Access Control) restricting an authenticated but unauthorized account from doing actions to your resources (https://azure.microsoft.com/en-us/documentation/articles/role-based-access-control-configure/).

Solution

You can change the code around a bit to tell it which account to use when performing the actions. Change the "GetAccessTokenUsingUserCredentials" method in the sample app to look like this:

    private static AuthenticationResult GetAccessTokenUsingUserCredentials(UserCredential userCredential)
    {
        AuthenticationContext authContext = new AuthenticationContext
            ("https://login.windows.net/" /* AAD URI */
            + "YOU.onmicrosoft.com" /* Tenant ID or AAD domain */);

        AuthenticationResult token = authContext.AcquireToken(
            "https://management.azure.com/"/* the Azure Resource Management endpoint */,
            "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" /* application client ID from AAD*/,
            new Uri("XXXX" /* redirect URI */,
            PromptBehavior.Auto,
            new UserIdentifier(userCredential.UserName, UserIdentifierType.RequiredDisplayableId));

        return token;
    }

and in the main function change the line that gets the token to be:

        var token = GetAccessTokenUsingUserCredentials(new UserCredential("<desired_account_email>"));

You should be able to modify it a bit to also take in a password so the AAD login box doesn't pop-up.

I hope that works for you!

EDIT: Here is how to get it to work for a Web-App:

  1. Get your Client ID and Key from the Configure page for the web app (You need to select a duration for the key and then click save for it to be generated)
  2. Change the authentication code in the app to look like this:

    private static AuthenticationResult GetAccessTokenForWebApp()
    {
        AuthenticationContext authContext = new AuthenticationContext
            ("https://login.windows.net/" /* AAD URI */
            + "YOU.onmicrosoft.com" /* Tenant ID or AAD domain */);
    
        ClientCredential cc = new ClientCredential("<client_id>", "<key>");
    
        AuthenticationResult token = authContext.AcquireToken(
            "https://management.azure.com/"/* the Azure Resource Management endpoint */,
            cc);
    
        return token;
    }
    
  3. At this point if you try to run the app you will get an authentication error that looks something like this:

Hyak.Common.CloudException AuthorizationFailed: The client 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' with object id 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' does not have authorization to perform action 'Microsoft.Sql/servers/write' over scope '/subscriptions/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/resourceGroups/{myResourceGroupName}/providers/Microsoft.Sql/servers/{newServerName}'.

This is because you need to add a RBAC role for the application/service principal. From the error message you need to copy the "Object ID", this is the AD object ID that will be used for the role assignment.

  1. Get the latest powershell cmdlets for Windows Azure: http://azure.microsoft.com/en-us/downloads/
  2. Once powershell is installed run the following:

    Switch-AzureMode AzureResourceManager #_this will be deprecated soon_
    Add-AzureAccount # you will be prompted for your credentials
    New-AzureRoleAssignment -ObjectId <your_object_id> -RoleDefinitionName <role_name> 
    # You can use "Contributor" to give the web app access to basically everything.
    # You can look in portal.azure.com for the roles that are available
    
    1. When the above runs you should see output that the object id was added to the role.

Your Web-App should now work!

Some bonus reading:

For service principal / app login: http://azure.microsoft.com/en-us/documentation/articles/resource-group-authenticate-service-principal/

For on-behalf-of login: https://msdn.microsoft.com/en-us/library/azure/dn790557.aspx

1
votes

I wrote a console app to test various scenarios. Though I need to try more cases, only two of the ones I have implemented succeed at this point:

  • NativeClientApp with ClientId_RedirectUri_PromptBehavior
  • NativeClientApp with ClientId_RedirectUri_PromptBehavior_UserIdentifier

The source code is below if you are interested. Also, I am only testing the creation of resource groups for now, because that is much less intensive that creating Sql Server instances. Both seem to produce the same errors.

Initial Results

The result output shows the case, the outcome, and the cue to continue.

WebApp with ClientCertificate
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientAssertion
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientCredential
AuthorizationFailed: The client '7a31a564-20ba-4ac1-a2ee-4f5e35a70dcc' with object id '7a31a564-20ba-4ac1-a2ee-4f5e35a70dcc' does not have authorization to perform action 'Microsoft.Resources/subscriptions/resourcegroups/write' ov
er scope '/subscriptions/14929cfc-3501-48cf-a5c9-b24a7daaf694/resourcegroups/MY_RESOURCE_GROUP_NAME635776010237527878'.
Press any key to continue.

WebApp with ClientId_UserAssertion
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientCredential_UserAssertion
AADSTS50027: Invalid JWT token. AADSTS50027: Invalid JWT token. Token format not valid.
Trace ID: a64f0683-23ae-4461-8546-55293f7ff1d3
Correlation ID: 62cc80e9-f013-4a74-8031-3294e69d4478
Timestamp: 2015-09-12 03:43:57Z
Press any key to continue.

WebApp with ClientAssertion_UserAssertion
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientCertificate_UserAssertion
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientId_RedirectUri
The method or operation is not implemented.
Press any key to continue.

WebApp with ClientId_UserCredential
missing_federation_metadata_url: Federation Metadata Url is missing for federated user. This user type is unsupported.
Press any key to continue.

WebApp with ClientId_RedirectUri_PromptBehavior
AADSTS90014: The request body must contain the following parameter: 'client_secret or client_assertion'.
Trace ID: 06fde160-bd2b-4f16-b49e-0f0ff8e17f48
Correlation ID: baabb83f-cebb-48ba-b2be-1efb53ec3121
Timestamp: 2015-09-12 03:44:21Z
Press any key to continue.

WebApp with ClientId_RedirectUri_PromptBehavior_UserIdentifier
AADSTS90014: The request body must contain the following parameter: 'client_secret or client_assertion'.
Trace ID: 6110ef02-a6b0-4e41-b0e5-db97b13c66ce
Correlation ID: e6f6526a-8395-480a-8ac7-b75903b324d9
Timestamp: 2015-09-12 03:44:30Z
Press any key to continue.

WebApp with ClientId_RedirectUri_PromptBehavior_UserIdentifier_ExtraQueryParams
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientCertificate
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientAssertion
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientCredential
Value cannot be null.
Parameter name: clientSecret
Press any key to continue.

NativeClientApp with ClientId_UserAssertion
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientCredential_UserAssertion
Value cannot be null.
Parameter name: clientSecret
Press any key to continue.

NativeClientApp with ClientAssertion_UserAssertion
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientCertificate_UserAssertion
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientId_RedirectUri
The method or operation is not implemented.
Press any key to continue.

NativeClientApp with ClientId_UserCredential
missing_federation_metadata_url: Federation Metadata Url is missing for federated user. This user type is unsupported.
Press any key to continue.

NativeClientApp with ClientId_RedirectUri_PromptBehavior
Success! Created MY_RESOURCE_GROUP_NAME635776011040240760
Press any key to continue.

NativeClientApp with ClientId_RedirectUri_PromptBehavior_UserIdentifier
Success! Created MY_RESOURCE_GROUP_NAME635776011079693849
Press any key to continue.

NativeClientApp with ClientId_RedirectUri_PromptBehavior_UserIdentifier_ExtraQueryParams
The method or operation is not implemented.
Press any key to continue.

Testing complete.:)

Program.cs

using System;
using Microsoft.Azure;
using Microsoft.Azure.Management.Resources;
using Microsoft.Azure.Management.Resources.Models;
using Microsoft.Azure.Management.Sql;
using Microsoft.Azure.Management.Sql.Models;
using Microsoft.IdentityModel.Clients.ActiveDirectory;

namespace CreateNewAzureSQLServer
{
    class Program
    {
        private static string

            domain = "MY_DOMAIN.onmicrosoft.com",
            resource = "https://management.azure.com/",
            subscriptionId = "xxxxx-xxxxx-xxxxx-xxxxx",

            // web
            clientId_web = "xxxxx-xxxxx-xxxxx-xxxxx",
            clientSecret_web = "xxxxx=",
            redirectUri_web = "http://myWebApp",

            // native
            clientId_native = "xxxxx-xxxxx-xxxxx-xxxxx",
            clientSecret_native = string.Empty,
            redirectUri_native = "http://myNativeClientApp",

            // adminstrator
            userName = "MY_USERNAME",
            userPassword = "MY_PASSWORD",

            // create
            adminAccountName = "MY_ADMIN_ACCOUNT_NAME",
            adminAccountPwd = "MY_ACCOUNT_ADMIN_PWD",
            resourceGroupName = "MY_RESOURCE_GROUP_NAME",
            serverName = "MY_SERVER_NAME",
            location = "West US";

        private static AuthenticationResult GetAccessToken(
            string clientId, string redirectUri, string clientSecret, AuthType type)
        {
            var authority = "https://login.windows.net/" + domain;
            var authContext = new AuthenticationContext(authority);
            authContext.TokenCache.Clear();
            AuthenticationResult token = null;

            switch (type)
            {
                case AuthType.ClientCertificate:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientAssertion:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientCredential:                       
                    token = authContext.AcquireToken(resource,
                        new ClientCredential(clientId, clientSecret));
                    break;

                case AuthType.ClientId_UserAssertion:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientCredential_UserAssertion:
                    token = authContext.AcquireToken(resource,
                        new ClientCredential(clientId, clientSecret),
                        new UserAssertion(userPassword, "username", userName));
                    break;

                case AuthType.ClientAssertion_UserAssertion:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientCertificate_UserAssertion:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientId_RedirectUri:
                    throw new NotImplementedException();
                    break;

                case AuthType.ClientId_UserCredential:
                    token = authContext.AcquireToken(resource,
                        clientId,
                        new UserCredential(userName, userPassword));
                    break;

                case AuthType.ClientId_RedirectUri_PromptBehavior:
                    token = authContext.AcquireToken(resource,
                        clientId,
                        new Uri(redirectUri),
                        PromptBehavior.Auto);
                    break;

                case AuthType.ClientId_RedirectUri_PromptBehavior_UserIdentifier:                        
                    var cred = new UserCredential(userName);
                    token = authContext.AcquireToken(resource,
                        clientId,
                        new Uri(redirectUri),
                        PromptBehavior.Auto,
                        new UserIdentifier(cred.UserName, UserIdentifierType.RequiredDisplayableId));
                    break;

                case AuthType.ClientId_RedirectUri_PromptBehavior_UserIdentifier_ExtraQueryParams:
                    throw new NotImplementedException();
                    break;

                default:
                    break;
            };

            return token;
        }

        static void CreateSqlServer(TokenCloudCredentials creds)
        {
            var client = new SqlManagementClient(creds);
            var someProperties = new ServerCreateOrUpdateProperties
            {
                AdministratorLogin = adminAccountName,
                AdministratorLoginPassword = adminAccountPwd,
                Version = "12"
            };

            var parameters = new ServerCreateOrUpdateParameters(someProperties, location);
            ServerGetResponse aCreateResponse
                = client.Servers.CreateOrUpdate(resourceGroupName, serverName, parameters);
        }

        static string CreateResourceGroup(TokenCloudCredentials creds)
        {
            var uniqueResourceGroupName = resourceGroupName + DateTime.Now.Ticks.ToString();
            var resourceClient = new ResourceManagementClient(creds);
            var resourceGroupParameters = new ResourceGroup()
            {
                Location = location
            };
            var resourceGroupResult = resourceClient
                .ResourceGroups
                .CreateOrUpdate(uniqueResourceGroupName, resourceGroupParameters);

            return uniqueResourceGroupName;
        }

        static void Main(string[] args)
        {
            foreach (AppType appType in Enum.GetValues(typeof(AppType)))
            {
                var clientId = appType == AppType.WebApp ? clientId_web : clientId_native;
                var clientSecret = appType == AppType.WebApp ? clientSecret_web : clientSecret_native;
                var redirectUri = appType == AppType.WebApp ? redirectUri_web : redirectUri_native;

                foreach (AuthType authType in Enum.GetValues(typeof(AuthType)))
                {
                    try
                    {
                        Console.WriteLine(appType.ToString() + " with " + authType.ToString());
                        var token = GetAccessToken(clientId, redirectUri, clientSecret, authType);
                        var creds = new TokenCloudCredentials(subscriptionId, token.AccessToken);
                        var resourceGroupName = CreateResourceGroup(creds);
                        Console.WriteLine("Success! Created " + resourceGroupName);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }

                    Console.WriteLine("Press any key to continue.");
                    Console.ReadLine();
                }
            }

            Console.WriteLine("Testing complete.:)");
            Console.ReadLine();

            //CreateSqlServer(creds);
        }

        enum AppType
        {
            WebApp,
            NativeClientApp
        }

        enum AuthType
        {
            ClientCertificate,
            ClientAssertion,
            ClientCredential,
            ClientId_UserAssertion,
            ClientCredential_UserAssertion,
            ClientAssertion_UserAssertion,
            ClientCertificate_UserAssertion,
            ClientId_RedirectUri,
            ClientId_UserCredential,
            ClientId_RedirectUri_PromptBehavior,
            ClientId_RedirectUri_PromptBehavior_UserIdentifier,
            ClientId_RedirectUri_PromptBehavior_UserIdentifier_ExtraQueryParams
        }
    }
}

packages.config

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Hyak.Common" version="1.0.2" targetFramework="net452" />
  <package id="Microsoft.Azure.Common" version="2.1.0" targetFramework="net452" />
  <package id="Microsoft.Azure.Common.Authentication" version="1.1.5-preview" targetFramework="net452" />
  <package id="Microsoft.Azure.Common.Dependencies" version="1.0.0" targetFramework="net452" />
  <package id="Microsoft.Azure.Management.Resources" version="2.18.7-preview" targetFramework="net452" />
  <package id="Microsoft.Azure.Management.Sql" version="0.38.0-prerelease" targetFramework="net452" />
  <package id="Microsoft.Bcl" version="1.1.9" targetFramework="net452" />
  <package id="Microsoft.Bcl.Async" version="1.0.168" targetFramework="net452" />
  <package id="Microsoft.Bcl.Build" version="1.0.14" targetFramework="net452" />
  <package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="2.18.206251556" targetFramework="net452" />
  <package id="Microsoft.Net.Http" version="2.2.22" targetFramework="net452" />
  <package id="Microsoft.Rest.ClientRuntime" version="1.2.0" targetFramework="net452" />
  <package id="Microsoft.Rest.ClientRuntime.Azure.Authentication" version="0.9.3" targetFramework="net452" />
  <package id="Newtonsoft.Json" version="6.0.4" targetFramework="net452" />
</packages>