Tuesday, March 29, 2016

Adal.js does not get tokens for external api endpoint resource

Leave a Comment

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;                 }); 

2 Answers

Answers 1

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.

Answers 2

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". Upload the manifest back to API application.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment