Troubles authenticating native client to Azure AD securised MVC web app

Viewed 541

I created both a MVC 5 web app hosted on Azure and a WPF client. My short term purpose (as if I can achieve that I'll be able to implement all my uses case) is the following:

  • Enforce Azure Ad authentification on the WPF client
  • Have the MVC web app to check through Azure Graph API the AD group membership of the user authentified in the client
  • Send back Graph API object to the client (IUser, Group...)
  • Use group membership to define Authorization on controllers

My actual issue is the following: The user launch the app, and is prompted for authentication. I guess it work as I can display the user's mail and I have an access token. The user tries to access a web api controller and it works fine The user tries to access another web api controller decorated with [Authorize] and i get in return some HTML page stating this : "We can't sign you in.Your browser is currently set to block JavaScript. You need to allow JavaScript to use this service."

From what I've found searching on the web it seems that it could be related to my web app that is not configured properly (I already tried to add my webapp url in trusted sites and I'm sure that my controller URL is Ok). i cannot find much doc on native client + AAD + MVC so I don't really know how to correct it.

Here's my startup.auth.cs from the webapp :

public partial class Startup
{

    private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
    private static string appKey = ConfigurationManager.AppSettings["ida:AppKey"];
    private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
    private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
    private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
    private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
    private static string certName = ConfigurationManager.AppSettings["ida:CertName"];

    public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

    string graphResourceId = ConfigurationManager.AppSettings["ida:GraphUrl"];

    public void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
        app.UseCookieAuthentication(new CookieAuthenticationOptions());  

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {

                ClientId = clientId,
                Authority = Authority,
                PostLogoutRedirectUri = postLogoutRedirectUri,

                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                    //
                    // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
                    //

                    AuthorizationCodeReceived = (context) =>
                    {
                        var code = context.Code;

                        #region Certs (not used)
                        if (certName.Length != 0)
                        {
                            // Create a Client Credential Using a Certificate
                            //
                            // Initialize the Certificate Credential to be used by ADAL.
                            // First find the matching certificate in the cert store.
                            //

                            X509Certificate2 cert = null;
                            X509Store store = new X509Store(StoreLocation.CurrentUser);
                            try
                            {
                                store.Open(OpenFlags.ReadOnly);
                                // Place all certificates in an X509Certificate2Collection object.
                                X509Certificate2Collection certCollection = store.Certificates;
                                // Find unexpired certificates.
                                X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
                                // From the collection of unexpired certificates, find the ones with the correct name.
                                X509Certificate2Collection signingCert = currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, false);
                                if (signingCert.Count == 0)
                                {
                                    // No matching certificate found.
                                    return Task.FromResult(0);
                                }
                                // Return the first certificate in the collection, has the right name and is current.
                                cert = signingCert[0];
                            }
                            finally
                            {
                                store.Close();
                            }

                            // Then create the certificate credential.
                            ClientAssertionCertificate credential = new ClientAssertionCertificate(clientId, cert);

                            string userObjectID = context.AuthenticationTicket.Identity.FindFirst(
                                "http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
                            AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
                            AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                                code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
                            AuthenticationHelper.token = result.AccessToken;
                        } 
                        #endregion
                        else
                        {
                            // Create a Client Credential Using an Application Key
                            ClientCredential credential = new ClientCredential(clientId, appKey);
                            string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
                            AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
                            AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(
                                code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
                            AuthenticationHelper.token = result.AccessToken;
                        }


                        return Task.FromResult(0);
                    }

                }

            });
    }
}

Here's the controller which can be acceded when not decorated with [Authorize] but in that case the action throw a null exception (but if I can't get it fixed i'll post another question):

[System.Web.Http.Authorize]
public class UserADGraphController : ApiController
{

    [ResponseType(typeof(IUser))]
    [System.Web.Http.Route("api/UserADGraphController/GetMyInformations")]
    public IHttpActionResult GetMyInformations()
    {
        try
        {
            string uID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
            if (uID == null)
                return Ok("UId null");
            ActiveDirectoryClient client = AuthenticationHelper.GetActiveDirectoryClient();
            if (client == null)
                return Ok("Client null");
            IUser adUser = client.Users.Where(u => u.ObjectId == uID).ExecuteAsync().Result.CurrentPage.SingleOrDefault();

            if (adUser == null)
            {
                return NotFound();
            }

            return Ok(adUser);
        }
        catch (Exception e)
        {
            return Ok(e.Message + " " + e.StackTrace);
        }

and finally here are relevant parts of the client:

In the mainviewmodel class:

#region Azure AD auth properties
    private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
    private static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
    private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
    Uri redirectUri = new Uri(ConfigurationManager.AppSettings["ida:RedirectUri"]);

    private static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

    private static string AppServiceResourceId = ConfigurationManager.AppSettings["todo:AppServiceResourceId"];
    private static string AppServiceBaseAddress = ConfigurationManager.AppSettings["todo:AppServiceBaseAddress"];

    private HttpClient httpClient;
    private AuthenticationContext authContext = null;
    #endregion

In the mainviewmodel constructor:

authContext = new AuthenticationContext(authority);
httpClient = new HttpClient();

My sign in method:

{
            AuthenticationResult result = null;
            try
            {
                result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);
                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
                SignInLabelContent = "Connected to azure AD as " + result.UserInfo.DisplayableId;

            }
            catch (AdalException ex)
            {
                if (ex.ErrorCode == "user_interaction_required")
                {

                }
                else
                {
                    // An unexpected error occurred.
                    string message = ex.Message;
                    if (ex.InnerException != null)
                    {
                        message += "Inner Exception : " + ex.InnerException.Message;
                    }
                    Messenger.Default.Send<NotificationMessage>(new NotificationMessage(message));
                    //MessageBox.Show(message);
                }
                return;
            }
        }

The method that access the protected controller:

IUser me = null;

            AuthenticationResult result = null;

            result = authContext.AcquireToken(AppServiceResourceId, clientId, redirectUri, PromptBehavior.Auto);

            string authHeader = result.CreateAuthorizationHeader();
            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
            //HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");
            //request.Headers.TryAddWithoutValidation("Authorization", authHeader);
            //HttpResponseMessage response = await client.SendAsync(request);
            //string responseString = await response.Content.ReadAsStringAsync();
            //LogManager.log(responseString);
            //Messenger.Default.Send<NotificationMessage>(new NotificationMessage(responseString));

            HttpResponseMessage response = await httpClient.GetAsync(AppServiceBaseAddress + "/api/UserADGraphController/GetMyInformations");

            if (response.IsSuccessStatusCode)
            {
                var jsonString = await response.Content.ReadAsStringAsync();
                LogManager.log(jsonString);
                me = JsonConvert.DeserializeObject<IUser>(jsonString);

                //Messenger.Default.Send<NotificationMessage>(new NotificationMessage(jsonString));

            }

In my case response has status code 200 but the jsonString contains the web page telling me about javascript disabled.

If someone has an idea it would be great !

Thanks !

2 Answers
Related