2
votes

I've created a Web API which uses Azure Active Directory for its authentication. It uses a multi-tenant AAD. To test it, I also created a console app which uses the ADAL library to authenticate against AAD so I can access my API. In the main AAD tenant all is working well, because I don't need to grant anything. But when accessing the app from a second tenant, I first trigger the admin consent flow (adding a prompt=admin_consent). But when I exit and open the app again, if I try to login with a user with no admin rights on the AAD, it tries to open the user consent and it fails (because the users don't have right to allow access to the AAD). If I already given admin consent, shouldn't the users already be consented?

The code for the test app is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Threading.Tasks;
using System.Web;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;

namespace TestConsole
{
    internal class Program
    {
        private const string _commonAuthority = "https://login.microsoftonline.com/common/";

        private static void Main(string[] args)
        {
            ConsoleKeyInfo kinfo = Console.ReadKey(true);
            AuthenticationContext ac = new AuthenticationContext(_commonAuthority);
            while (kinfo.Key != ConsoleKey.Escape)
            {
                if (kinfo.Key == ConsoleKey.A)
                {
                    AuthenticationResult ar = ac.AcquireToken("https://babtecportal.onmicrosoft.com/Portal2015.Api", "client_id", new Uri("https://out.es"), PromptBehavior.Auto, UserIdentifier.AnyUser, "prompt=admin_consent");
                }
                else if (kinfo.Key == ConsoleKey.C)
                {
                    Console.WriteLine("Token cache length: {0}.", ac.TokenCache.Count);
                }
                else if (kinfo.Key == ConsoleKey.L)
                {
                    ac.TokenCache.Clear();
                    HttpClient client = new HttpClient();
                    var request = new HttpRequestMessage(HttpMethod.Get, _commonAuthority + "oauth2/logout?post_logout_redirect_uri=" + HttpUtility.UrlEncode("https://out.es"));
                    var response=client.SendAsync(request).Result;
                    Console.WriteLine(response.StatusCode);
                    ac=new AuthenticationContext(_commonAuthority);
                }
                else
                {
                    int num;
                    if (int.TryParse(Console.ReadLine(), out num))
                    {
                        try
                        {
                            AuthenticationResult ar = ac.AcquireToken("https://babtecportal.onmicrosoft.com/Portal2015.Api", "client_id", new Uri("http://out.es"),PromptBehavior.Auto,UserIdentifier.AnyUser);

                            ac = new AuthenticationContext(ac.TokenCache.ReadItems().First().Authority);
                            // Call Web API
                            string authHeader = ar.CreateAuthorizationHeader();
                            HttpClient client = new HttpClient();
                            HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, string.Format("http://localhost:62607/api/Values?num={0}", num));
                            request.Headers.TryAddWithoutValidation("Authorization", authHeader);
                            HttpResponseMessage response = client.SendAsync(request).Result;
                            if (response.IsSuccessStatusCode)
                            {
                                string responseString = response.Content.ReadAsStringAsync().Result;
                                Values vals = JsonConvert.DeserializeObject<Values>(responseString);
                                Console.WriteLine("Username: {0}", vals.Username);
                                Console.WriteLine("Name: {0}", vals.FullName);
                                vals.Range.ToList().ForEach(Console.WriteLine);
                            }
                            else
                            {
                                Console.WriteLine("Status code: {0}", response.StatusCode);
                                Console.WriteLine("Reason: {0}", response.ReasonPhrase);
                            }
                        }
                        catch (AdalException ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    }
                }
                kinfo = Console.ReadKey(true);
            }
        }
    }

    public class Values
    {
        public string Username { get; set; }
        public string FullName { get; set; }
        public IEnumerable<int> Range { get; set; }
    }
}
1

1 Answers

5
votes

Your test app is a native client. In OAuth terms it is a public client. Those terms apply to any client that does not have a client secret or certificate credential of its own. The admin consent feature does not apply to native clients and only works for web applications. Ideally, there would be an error returned when admin consent is attempted for a native app that would indicate that the combination is not supported. We are going to look in to returning such an error in the future to prevent this kind of confusion.

In the meantime, there is no way to prevent users from seeing the consent dialogue when they sign in to a native client.

The situation is somewhat more complicated if the native app is calling a web api where both the native app and web api are owned by the same vendor/tenant. If this is set up correctly then the user will see a combined consent dialog that allows the user to consent to both the native app as well as the web api. The consent to the web api will be recorded permanently. The consent to the native app will only apply to that sign in session in the same way it would if no web api were involved. If a web api is involved in this way then admin consent can be invoked. The admin can then consent to the web api on behalf of all users. However, individual users will still need to consent to the native app.

To correctly set up this consent chain you need to use the 'knownClientApplication' attribute in the application manifest of the web api. You set the value of this attribute to the client id of the native app. You can see this being done in this sample:

https://github.com/AzureADSamples/NativeClient-WebAPI-MultiTenant-WindowsStore/blob/master/README.md

Essentially you download the application manifest through the portal, update this particular value, and then upload it.

There is some more comprehensive documentation on these topics here:

https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx

Update: One of the stipulations in the above explanation of a native app calling a web api was that they both had to be in the same tenant. If they are not in the same tenant then things get more complicated. This is the case when an ISV has created a web API that they want to make available to apps written by customers. In order for an app to get a token for a resource both apps must be registered in the same tenant. Thus, the first thing the customer will need to do is get the web api registered in their own tenant. If the web api is in the app gallery then they simply go there and install the app. The ISV does not have to have their app in the app gallery to allow customers to register it, but registration gets more complicated. The ISV will need to create a web site, registered in the ISV tenant, that the customer admin can visit. That website needs sign in the admin to get a token for the web api in a way that will trigger the consent process. Once that is complete, then the api will be registered in the customer tenant and available to customer apps.

To get your app in to the app gallery follow the instructions near the bottom of this page:

http://azure.microsoft.com/en-us/marketplace/active-directory/