Adal.js 无法获取外部 API 终结点资源的令牌

16

我正在使用 adal.js 与一个 Angular SPA(单页应用程序)网站尝试获取来自外部 Web API 站点(不同域)的数据。使用 adal.js 进行 SPA 认证很容易,但当需要承载令牌时,让其与 API 通信完全不起作用。除了无数博客之外,我还使用了 https://github.com/AzureAD/azure-activedirectory-library-for-js 作为模板。

问题在于,当我在初始化 adal.js 时设置端点时,它似乎会将所有外部端点流量重定向到 Microsoft 的登录服务。

观察结果:

  • Adal.js 会话存储包含两个 adal.access.token.key 条目。一个用于 SPA Azure AD 应用程序的客户端 ID,另一个用于外部 api。只有 SPA 令牌有值。
  • 如果我不将 $httpProvider 注入到 adal.js 中,则调用会发送到外部 API,并且我会得到 401 响应。
  • 如果我手动将 SPA 令牌添加到 http 标头中(授权:bearer 'token value'),则会得到 401 响应。

我的理论是,adal.js 无法检索端点的令牌(可能是因为我在 SPA 中配置了错误的内容),并且它停止流量到端点,因为无法获取所需的令牌。SPA 令牌不能用于 API,因为它不包含所需的权限。 为什么 adal.js 无法为端点获取令牌,我该如何解决?

附加信息:

  • 客户端 Azure AD 应用程序已配置为针对 API 使用委派权限,并在应用程序清单中启用 oauth2AllowImplicitFlow = true。
  • API Azure AD 应用程序已配置为模拟和 oauth2AllowImplicitFlow = true(不认为这是必需的,但尝试了一下)。它是多租户的。
  • API 已配置为允许所有 CORS 来源,并且当使用模拟 (hybrid MVC (Adal.net) + Angular) 的另一个 Web 应用程序时,它可以正常工作。

会话存储:

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和adal配置文件)

(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代码:

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

我想我可能有同样的问题。你解决了这个问题吗? - Juha
1
在这上面发起了悬赏。我和你一模一样的情况,尝试了我能想到的所有方法,阅读了 ADAL 快速入门 Gtithub 网站上的所有信息,甚至买了 Vittorio 的该死的书!我简直无法相信这样一个简单的场景居然不能正常工作!! - Shailen Sukul
1
@BjørnF.Rasmussen 我已经在 https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi/issues/12 上全面记录了我的问题,并且与你的问题非常相似。如果你想联系我,或许我们可以交流一些关于这个问题的想法。谢谢。 - Shailen Sukul
@BjørnF.Rasmussen 请求中的授权头是否会在重定向时被丢弃,因此问题在于它在尝试识别承载令牌之前重定向到登录页面...?我也遇到了类似的困难。 - Phish
@Phish:我不确定。据我所知,持有者令牌应该在初始登录后包含所需的权限,以便可以直接用于 API。 - Bjørn F. Rasmussen
显示剩余7条评论
4个回答

2

好的,我一直在努力解决这个问题。我想让我的ADAL.js SPA应用程序(不使用angular)成功地进行跨域XHR请求,以访问我的宝贵的CORS启用的Web API。

像我这样的新手都在使用的这个示例应用程序有这个问题:它具有从同一域服务的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... 因此,对于与您的WebAPI通信的每个SPA客户端,您都需要将SPA注册的ClientID放入该数组中...

它应该看起来像这样...

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

很高兴你已经让事情按照你想要的方式工作了,但通常这并不是你想要设置的方式。通常,你需要使用ADAL.js来将用户登录到你的SPA(听起来你已经完成了),然后让你的SPA请求ADAL.js提供一个访问令牌来调用你的WebAPI。听起来你的SPA正在使用访问SPA应用程序的访问令牌调用WebAPI(由于你在这里列出的代码),而不是使用它来获取此应用程序的访问令牌。 - Jonathan Rupp

1
你需要让你的Web API意识到你的客户端应用程序。仅从客户端添加委派权限是不够的。
要让API客户端意识到,前往Azure管理门户,下载API清单,并将客户端应用程序的ClientID添加到“knownClientApplications”列表中。
为了允许Implicit流,您还需要在清单中将“oauth2AllowImplicitFlow”设置为true。
将清单上传回API应用程序。

当使用adal.net时,这不是必需的,因为我有使用adal.net和针对API的模拟解决方案。这是adal.js的特殊要求吗?我也尝试过这个,但它没有起作用 :( - Bjørn F. Rasmussen
@BjørnF.Rasmussen 我曾在GitHub的这个帖子中问过类似的问题,得到了一些反馈,我尝试了一下,但对我来说并没有起作用。请检查一下是否适用于您:https://github.com/Azure-Samples/active-directory-dotnet-webapi-multitenant-windows-store/issues/7#issuecomment-188923994 - Pavel Pikat

1
ADAL.js除了id_token之外,还可以获取访问不同域上运行的Azure AD保护的API的access_token。在登录期间,它只使用id_token,该令牌可用于访问同一域中的资源。但是,在调用在不同域中运行的API时,adal拦截器会检查API URL是否配置在adal.init()作为终结点。
只有这样才会为请求的资源调用访问令牌。这还需要将SPA配置为可以访问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
    );

2. 允许Azure AD中的SPA应用程序访问API应用程序: 输入图像描述

您可以参考此链接了解详细信息:ADAL.js深入研究


谢谢您的反馈。这似乎与我尝试过的相同,但可能是在较新版本的adal.js中有效。我计划在明年初再次尝试它。 - Bjørn F. Rasmussen
有人能更详细地解释一下App ID URI部分吗? 我已经让SPA应用程序工作并进行了身份验证,但是当我尝试对Web API进行身份验证时,它无法正常工作。当我查看来自我的Angular 2应用程序的HTTP标头时,我看不到任何带有身份验证信息的标头。这使我相信我没有提供正确的端点信息。如果我的根位置是https://blabla.azurewebsites.net/,那么我的应用程序ID URI也可以是https://blabla.azurewebsites.net/吗? - Magnus Gudmundsson

0

我不确定我们的设置是否完全相同,但我认为它是可比较的。

我有一个使用Azure API Management(APIM)通过外部Web API的Angular SPA。我的代码可能不是最佳实践,但到目前为止它对我有效 :)

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

请告诉我这是否有帮助,或者我是否可以提供进一步的帮助。


员工控制器是否与Angular SPA在同一台计算机上?即它们是否在同一个MVC项目中? - Bjørn F. Rasmussen
是的,抱歉没有澄清。 我有一个SPA的解决方案,其中包含前端JavaScript以及EmployeeController。 现在,EmployeeController用于调用实际后端Web API,其中包含真正的逻辑。 - Jonas
我明白了。所以你还没有能够让adal.js直接针对外部API使用模拟身份。EmployeeController是为了解决这个限制而引入的吗?当我们启动项目(它被推迟了)时,可能会得出类似的解决方案 :) - Bjørn F. Rasmussen
啊!糟糕,那我误解了。是的,就像你说的,这是一个变通方法。我无法直接从Javascript与外部API进行通信。对于造成的混淆我向你道歉。 - Jonas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接