Adal.js не получает токены для внешнего ресурса конечной точки api

Я пробовал adal.js с веб-сайтом Angular SPA (одностраничное приложение), который получает данные с внешнего веб-сайта API (другого домена). Аутентификация с помощью SPA была простой с adal.js, но получение ее для связи с API не работает вообще, когда требуются токены-носители. Я использовал https://github.com/AzureAD/azure-activedirectory-library-for-js в качестве шаблона в дополнение к бесчисленным блогам.

Проблема в том, что когда я устанавливаю конечные точки при запуске adal.js, adal.js, похоже, перенаправляет весь исходящий трафик конечной точки на службу входа в Microsoft.

Замечания:

  • Хранилище сеансов Adal.js содержит две записи adal.access.token.key. Один для идентификатора клиента приложения SPA Azure AD и один для внешнего api. Значение имеет только маркер SPA.
  • Если я не введу $ httpProvider в adal.js, тогда вызовы выходят во внешний API, и я получаю 401 в ответ.
  • Если я вручную добавлю токрен SPA в заголовок http (авторизация: значение токена на предъявителя), я получаю 401 в ответ.

Моя теория заключается в том, что adal.js не может извлекать токены для конечных точек (вероятно, из-за того, что я настроил что-то неправильно в SPA), и он останавливает трафик до конечной точки, поскольку он не может получить требуемый токен. Символ SPA не может использоваться против API, поскольку он не содержит требуемых прав. Почему adal.js не получает токенов для конечных точек и как я могу их исправить?

Дополнительная информация:

  • Приложение Azure AD клиента настроено на использование делегированных разрешений для API и oauth2AllowImplicitFlow = true в манифесте приложения.
  • Приложение API Azure AD настроено для олицетворения и oauth2AllowImplicitFlow = true (не думайте, что это необходимо, но попробовало). Это несколько арендаторов.
  • API сконфигурирован так, чтобы разрешить все происхождение CORS и работает корректно, когда используется другим веб-приложением с использованием олицетворения (гибридный MVC (Adal.net) + Angular).

Хранение сеансов:

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 (файл конфигурации с угловым и адальным)

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

Угловой код для вызова конечной точки:

$scope.getItems = function () {
            $http.get("https://localhost/Api/Items")
                .then(function (response) {                        
                    $scope.items = response.Items;
                });

Ответы

Ответ 1

Вам нужно, чтобы ваш веб-API знал о вашем клиентском приложении. Этого недостаточно, чтобы добавить делегированное разрешение API от вашего Клиента.

Чтобы узнать о клиенте API, перейдите на портал управления Azure, загрузите манифест API и добавьте ClientID вашего клиентского приложения в список "knownClientApplications". Загрузите манифест обратно в приложение API.

Ответ 2

ADAL.js получает access_token отдельно от id_token для вызова защищенного API Azure AD, работающего в другом домене. Первоначально во время входа в систему требуется только id_token. Этот токен имеет доступ для доступа к ресурсу того же домена. Но при вызове API, работающего в другом домене, перехватчик adal проверяет, настроен ли URL-адрес API в качестве конечной точки в adal.init().

Только тогда токен доступа вызывается для запрашиваемого ресурса. Это также требует, чтобы SPA был настроен в AAD для доступа к API APP.

Ключом к достижению этого является следующее: 1. Добавить конечные точки в 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. Дайте разрешение на приложение SPA в Azure AD для доступа к приложению API: enter image description here

Вы можете сослаться на эту ссылку для получения дополнительной информации: глубокое погружение ADAL.js

Ответ 3

Хорошо, я бил головой о стену, чтобы понять это. Попытка сделать мое приложение ADAL.js SPA (без углового) успешно сделать междоменные запросы XHR на мой драгоценный веб-API с поддержкой CORS.

Это примерное приложение, которое использует все новички, подобные мне, имеет эту проблему: в нем есть API и SPA, которые обслуживаются из одного домена, и требуется только одно приложение регистрации приложений AD Tenant. Это только путает вещи, когда приходит время разделить вещи на отдельные части.

Итак, из коробки, образец имеет этот Startup.Auth.cs, который работает нормально, насколько образец идет...

  public void ConfigureAuth(IAppBuilder app) {

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

но вам нужно изменить приведенный выше код, отбросить назначение Audience и перейти к массиву аудиторий. Это право: ValidAudiences. Итак, для каждого SPA-клиента, который разговаривает с вашим WebAPI, вы захотите ClientID вашей регистрации SPA в этом массиве...

Это должно выглядеть так...

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

РЕДАКТИРОВАТЬ

Хорошо, на основе обратной связи @JonathanRupp, я смог отменить решение Web API, которое я использовал выше, и смог изменить свой клиентский JavaScript, как показано ниже, чтобы все работало.

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

Ответ 4

Я не уверен, что наша настройка точно такая же, но я думаю, что это сопоставимо.

У меня есть Угловой SPA, который использует и внешний веб-API через Azure API Management (APIM). Мой код может быть не лучшим, но он работает для меня до сих пор :)

Приложение SPA Azure AD имеет делегированное разрешение на доступ к внешнему API-интерфейсу Azure AD.

SPA (основан на выборке Adal TodoList SPA)

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

Фрагмент из todoListSvc.js

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

Фрагменты из 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;
    }

Теперь, в моем внешнем интерфейсе API, мой Startup.Auth.cs выглядит так:

Внешний 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"
            });
    }

Пожалуйста, дайте мне знать, если это поможет или я могу оказать дополнительную помощь.