16

I'm trying out adal.js with an Angular SPA (Single Page Application) web site that gets data from an external Web API site (different domain). Authentication against the SPA was easy with adal.js, but getting it to communicate with the API is not working at all when bearer tokens are required. I have used https://github.com/AzureAD/azure-activedirectory-library-for-js as template in addition to countless blogs.

The problem is that when I set up endpoints while initiating adal.js, adal.js seems to redirect all outgoing endpoint traffic to microsofts login service.

Observations:

  • Adal.js session storage contains two adal.access.token.key entries. One for the client ID of the SPA Azure AD application and one for the external api. Only the SPA token has a value.
  • If I do not inject $httpProvider into adal.js, then calls go out to the external API and I get a 401 in return.
  • If I manually add the SPA token to the http header ( authorization: bearer 'token value') I get a 401 in return.

My theory is that adal.js is unable to retrieve tokens for endpoints (probably because I configured something wrong in the SPA) and it stops traffic to the endpoint since it is unable to get a required token. The SPA token cannot be used against the API since it does not contain the required rights. Why is adal.js not getting tokens for endpoints and how can I fix it?

Additional information:

  • The client Azure AD application is configured to use delegated permissions against the API and oauth2AllowImplicitFlow = true in app manifest.
  • The API Azure AD application is configured for impersonation and oauth2AllowImplicitFlow = true (do not think that is required, but tried it). It is multi tenant.
  • The API is configured to allow all CORS origins and it works correctly when used by another web app using impersonation (hybrid MVC (Adal.net) + Angular).

Session storage:

key (for the SPA application): adal.access.token.keyxxxxx-b7ab-4d1c-8cc8-xxx value: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1u...

key (for API application): adal.access.token.keyxxxxx-bae6-4760-b434-xxx
value:

app.js (Angular and adal configuration file)

(function () {
    'use strict';

    var app = angular.module('app', [
        // Angular modules 
        'ngRoute',

        // Custom modules 

        // 3rd Party Modules
        'AdalAngular'

    ]);

    app.config(['$routeProvider', '$locationProvider',
        function ($routeProvider, $locationProvider) {
        $routeProvider           

            // route for the home page
            .when('/home', {
                templateUrl: 'App/Features/Test1/home.html',
                controller: 'home'
            })

            // route for the about page
            .when('/about', {
                templateUrl: 'App/Features/Test2/about.html',
                controller: 'about',
                requireADLogin: true
            })

            .otherwise({
                redirectTo: '/home'
            })

        //$locationProvider.html5Mode(true).hashPrefix('!');

        }]);

    app.config(['$httpProvider', 'adalAuthenticationServiceProvider',
        function ($httpProvider, adalAuthenticationServiceProvider) {
            // endpoint to resource mapping(optional)
            var endpoints = {
                "https://localhost/Api/": "xxx-bae6-4760-b434-xxx",
            };

            adalAuthenticationServiceProvider.init(
                    {                        
                        // Config to specify endpoints and similar for your app
                        clientId: "xxx-b7ab-4d1c-8cc8-xxx", // Required
                        //localLoginUrl: "/login",  // optional
                        //redirectUri : "your site", optional
                        extraQueryParameter: 'domain_hint=mydomain.com',
                        endpoints: endpoints  // If you need to send CORS api requests.
                    },
                    $httpProvider   // pass http provider to inject request interceptor to attach tokens
                    );
        }]);
})();

Angular code for calling endpoint:

$scope.getItems = function () {
            $http.get("https://localhost/Api/Items")
                .then(function (response) {                        
                    $scope.items = response.Items;
                });
Bjørn F. Rasmussen
  • 359
  • 1
  • 4
  • 16
  • I think I might have the same problem. Did you figure this one out? – Juha Feb 04 '16 at 14:50
  • Nope. I Will let you know if I figure it out – Bjørn F. Rasmussen Feb 05 '16 at 17:46
  • 1
    Started a bounty on this. I am in the same exact situation as you and have tried everything I can think of, read every bit of information on the ADAL quickstart Gtithub site, even bought Vittorio's bloody book! I just cannot believe that such a simple scenario simply does not work!! – Shailen Sukul Mar 23 '16 at 05:13
  • 1
    @BjørnF.Rasmussen I have documented my troubles in full at https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi/issues/12 & it is really similar to yours. If you wanna contact me, maybe we can exchange some ideas about this issue. Thx. – Shailen Sukul Mar 23 '16 at 05:22
  • Thanks for your input @ShailenSukul. Because of these issues I have abandon the single page application approach for now and I'm working on an MVC6 architecture with SignalR instead. Let me know if you get it to work :) – Bjørn F. Rasmussen Mar 30 '16 at 07:39
  • Sad to hear about your decision to abandon this @BjørnF.Rasmussen and adding to my frustration at the #AzureAdal guys for such poor form. I am moving towards a solution where I get the token for the service in the controller of the client application and then injecting that token in a custom header in the $http call. Not ideal as I have to handle edge cases like expiry etc all myself. I still think that fetching the token server side is more stable as opposed to dealing with security zones etc client side. Will update when I have this working. – Shailen Sukul Mar 30 '16 at 23:36
  • That sounds like the way I made my previous web app, but that lead to some other issues with timeouts: http://stackoverflow.com/questions/26925463/identity-disappears-from-bearer-token-after-an-hour Hope you make it work :) – Bjørn F. Rasmussen Apr 01 '16 at 05:36
  • @ShailenSukul:Did you find any solution for this problem ? – Pickle Dec 06 '16 at 18:35
  • Nope, have not worked on this issue any more. Might pick it up again next year :) – Bjørn F. Rasmussen Dec 09 '16 at 06:26
  • @Pickle Sorry did not find a solution. Best bet is to do it server side in controller and call it from the front end. – Shailen Sukul Mar 07 '17 at 11:16
  • @BjørnF.Rasmussen Won't the authorisation header in the request get thrown away in the redirect, so the problem is with the fact that it is redirecting to a login before it attempts to recognise the bearer token...? I am facing slightly similar struggles. – Phish Apr 24 '17 at 13:26
  • @Phish: I am not sure. As I understand it, the bearer token should contain the required permissions after the initial login, so that it can be used directly against the api – Bjørn F. Rasmussen Apr 27 '17 at 05:28

4 Answers4

2

Ok, I've been bashing my head against the wall to figure this out. Trying to make my ADAL.js SPA app (sans angular) successfully make cross-domain XHR requests over to my precious CORS-enabled Web API.

This sample app, the one all the newbies like me are using, has this problem: it features an API and SPA all served from the same domain - and only requires a single AD Tenant app registration. This only confuses things when it comes time to pull things apart into separate pieces.

So, out of the box, the sample has this Startup.Auth.cs which works OK, as far as the sample goes...

  public void ConfigureAuth(IAppBuilder app) {

        app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Audience = ConfigurationManager.AppSettings["ida:Audience"],
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
            });
  }

but, you need to modify the above code, drop the Audience assignment, and go for an array of audiences.. That's right: ValidAudiences .. So, for every SPA client that is talking to your WebAPI, you'll want to put the ClientID of your SPA registration in this array...

It should look like this...

public void ConfigureAuth(IAppBuilder app)
{
    app.UseWindowsAzureActiveDirectoryBearerAuthentication(
        new WindowsAzureActiveDirectoryBearerAuthenticationOptions
        {
            Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
            TokenValidationParameters = new TokenValidationParameters
            {
                ValidAudiences = new [] { 
                 ConfigurationManager.AppSettings["ida:Audience"],//my swagger SPA needs this 1st one
                 "b2d89382-f4d9-42b6-978b-fabbc8890276",//SPA ClientID 1
                 "e5f9a1d8-0b4b-419c-b7d4-fc5df096d721" //SPA ClientID 2
                 },
                RoleClaimType = "roles" //Req'd only if you're doing RBAC 
                                        //i.e. web api manifest has "appRoles"
            }
        });
}

EDIT

Ok, based on @JonathanRupp's feedback, I was able to reverse out the Web API solution I was using shown above, and was able to modify my client JavaScript as shown below to make everything work.

    // Acquire Token for Backend
    authContext.acquireToken("https://mycorp.net/WebApi.MyCorp.RsrcID_01", function (error, token) {

        // Handle ADAL Error
        if (error || !token) {
            printErrorMessage('ADAL Error Occurred: ' + error);
            return;
        }

        // Get TodoList Data
        $.ajax({
            type: "GET",
            crossDomain: true,
            headers: {
                'Authorization': 'Bearer ' + token
            },
            url: "https://api.mycorp.net/odata/ToDoItems",
        }).done(function (data) {
            // For Each Todo Item Returned, do something
            var output = data.value.reduce(function (rows, todoItem, index, todos) {
                //omitted
            }, '');

            // Update the UI
            //omitted

        }).fail(function () {
            //do something with error
        }).always(function () {
            //final UI cleanup
        });
    });
bkwdesign
  • 1,953
  • 2
  • 28
  • 50
  • Glad you've got things working the way you want, but that's not usually how you want to set it up. Usually, you want to use ADAL.js to log the user into your SPA (which it sounds like you got working), then have your SPA ask ADAL.js for an access token to call your WebAPI. It sounds like you're having the SPA call WebAPI with the access token for the SPA's app (which WebAPI accepts because of the code you've listed here), instead of using that to get an access token for this app. – Jonathan Rupp Jun 30 '17 at 01:53
1

You need to make your Web API aware of your Client application. It's not enough to add delegated permission to API from your Client.

To make the API client aware, go to Azure management portal, download API's manifest and add ClientID of your Client application to the list of "knownClientApplications".

To allow Implicit flow you need to set "oauth2AllowImplicitFlow" to true in the manifest as well.

Upload the manifest back to API application.

Simon Bourdeau
  • 429
  • 5
  • 18
  • This is not a requirement when using adal.net, since I have working solutions that use adal.net and impersonation against the API. Is that a special requirement for adal.js? I have also tried this, but it did not work :( – Bjørn F. Rasmussen Feb 29 '16 at 06:43
  • @BjørnF.Rasmussen I tried asking similar question in this GitHub thread, and I got some feedback, which I tried, but it didn't work for me. Check if it works for you: https://github.com/Azure-Samples/active-directory-dotnet-webapi-multitenant-windows-store/issues/7#issuecomment-188923994 – Pavel Pikat Mar 01 '16 at 08:09
1

ADAL.js does get the access_token apart from id_token for calling Azure AD protected API running on different domain. Initially, during login, it only takes id_token. This token has the access for accessing resource of the same domain. But, on calling the API running in different domain, adal interceptor checks if the API URL is configured in as endpoint in adal.init().

It is only then that the access token is called for the requested resource. It also necessitates that the SPA is configured in the AAD to access API APP.

The key to achieve this is following: 1. Add endpoints in the adal.init()

var endpoints = {

    // Map the location of a request to an API to a the identifier of the associated resource
    //"Enter the root location of your API app here, e.g. https://contosotogo.azurewebsites.net/":
    //    "Enter the App ID URI of your API app here, e.g. https://contoso.onmicrosoft.com/TestAPI",
    "https://api.powerbi.com": "https://analysis.windows.net/powerbi/api",
    "https://localhost:44300/": "https://testpowerbirm.onmicrosoft.com/PowerBICustomServiceAPIApp"
};

adalProvider.init(
    {
        instance: 'https://login.microsoftonline.com/',
        tenant: 'common',
        clientId: '2313d50b-7ce9-4c0e-a142-ce751a295175',
        extraQueryParameter: 'nux=1',
        endpoints: endpoints,
        requireADLogin: true,

        //cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.  
        // Also, token acquisition for the To Go API will fail in IE when running on localhost, due to IE security restrictions.
    },
    $httpProvider
    );
  1. Give permission to the SPA application in Azure AD to access the API application: enter image description here

You may refer this link for details : ADAL.js deep dive

Rahul Mohan
  • 493
  • 3
  • 5
  • 18
  • Thx for your feedback. This seems to be the same that I tried, but maybe it's working in a newer version of adal.js. I plan to try it out again during start of next year – Bjørn F. Rasmussen Dec 30 '16 at 10:18
  • Can someone explain in more detail about the App ID URI part? I have got the SPA application working and authenticating , but when I try to authenticate to a web api, it does not work.. When I look at the http header coming from my angular 2 application I can't see any header with auth info. This leads me to believe that I'm not giving it the correct endpoint information. if my root location is https://blabla.azurewebsites.net/ can my app id uri also be https://blabla.azurewebsites.net/ ? – Magnus Gudmundsson Apr 06 '17 at 11:41
0

I'm not sure if our setup is exactly the same, but I think it it comparable.

I have a Angular SPA that uses and external Web API through Azure API Management (APIM). My code might not be best practice, but it works for me so far :)

The SPAs Azure AD app has a delegated permission to access the External APIs Azure AD app.

The SPA (is based upon the Adal TodoList SPA sample)

app.js

adalProvider.init(
    {
        instance: 'https://login.microsoftonline.com/', 
        tenant: 'mysecrettenant.onmicrosoft.com',
        clientId: '********-****-****-****-**********',//ClientId of the Azure AD app for my SPA app            
        extraQueryParameter: 'nux=1',
        cacheLocation: 'localStorage', // enable this for IE, as sessionStorage does not work for localhost.
    },
    $httpProvider
    );

Snippet from the todoListSvc.js

getWhoAmIBackend: function () {
        return $http.get('/api/Employee/GetWhoAmIBackend');
    },

Snippets from the EmployeeController

public string GetWhoAmIBackend()
    {
        try
        {
            AuthenticationResult result = GetAuthenticated();

            HttpClient client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

            var request = new HttpRequestMessage()
            {
                RequestUri = new Uri(string.Format("{0}", "https://api.mydomain.com/secretapi/api/Employees/GetWhoAmI")),
                Method = HttpMethod.Get, //This is the URL to my APIM endpoint, but you should be able to use a direct link to your external API

            };
            request.Headers.Add("Ocp-Apim-Trace", "true"); //Not needed if you don't use APIM
            request.Headers.Add("Ocp-Apim-Subscription-Key", "******mysecret subscriptionkey****"); //Not needed if you don't use APIM

            var response = client.SendAsync(request).Result;
            if (response.IsSuccessStatusCode)
            {
                var res = response.Content.ReadAsStringAsync().Result;
                return res;
            }
            return "No dice :(";
        }
        catch (Exception e)
        {
            if (e.InnerException != null)
                throw e.InnerException;
            throw e;
        }
    }

        private static AuthenticationResult GetAuthenticated()
    {
        BootstrapContext bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as BootstrapContext;
        var token = bootstrapContext.Token;

        Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext =
            new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext("https://login.microsoftonline.com/mysecrettenant.onmicrosoft.com");

        //The Client here is the SPA in Azure AD. The first param is the ClientId and the second is a key created in the Azure Portal for the AD App
        ClientCredential credential = new ClientCredential("clientid****-****", "secretkey ********-****");

        //Get username from Claims
        string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;

        //Creating UserAssertion used for the "On-Behalf-Of" flow
        UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);

        //Getting the token to talk to the external API
        var result = authContext.AcquireToken("https://mysecrettenant.onmicrosoft.com/backendAPI", credential, userAssertion);
        return result;
    }

Now, in my backend external API, my Startup.Auth.cs looks like this:

The external API Startup.Auth.cs

        public void ConfigureAuth(IAppBuilder app)
    {
        app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
                    SaveSigninToken = true
                },
                AuthenticationType = "OAuth2Bearer"
            });
    }

Please let me know if this helps or if I can be of further assistance.

Jonas
  • 146
  • 7
  • Is the EmployeeController on the same machine as the Angular SPA? I.e. are both in the same MVC project? – Bjørn F. Rasmussen Feb 23 '16 at 07:33
  • Yes, sorry for not clarifying that. I have one solution for the SPA, that contains frontend javascript, as well as the EmployeeController. Now, the EmployeeController is used to make calls to the actual backend web API where the real logic is. – Jonas Feb 23 '16 at 08:03
  • I see. So you have not been able to get adal.js to use impersonation directly against the external api. Is the EmployeeController introduced as a workaround for this limitation? It might be that we end up with a similar solution when we start up the project (it was postponed) :) – Bjørn F. Rasmussen Feb 24 '16 at 09:01
  • Ah! Darn, then I misunderstood. Yes, it is as you say, a workaround. I was not able to talk to the external API directly from javascript. Sorry for the confusion. – Jonas Feb 24 '16 at 09:38