【问题标题】:Adal.js does not get tokens for external api endpoint resourceAdal.js 没有获取外部 api 端点资源的令牌
【发布时间】:2015-09-02 11:47:42
【问题描述】:

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

问题是,当我在启动 adal.js 时设置端点时,adal.js 似乎将所有传出端点流量重定向到微软的登录服务。

观察:

  • Adal.js 会话存储包含两个 adal.access.token.key 条目。一个用于 SPA Azure AD 应用程序的客户端 ID,另一个用于外部 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)的 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;
                });

【问题讨论】:

  • 我想我可能有同样的问题。这个你搞定了吗?
  • 不。如果我弄明白了,我会告诉你的????
  • 开始赏金。我和你的情况完全一样,我已经尝试了我能想到的一切,阅读了 ADAL quickstart Gtithub 网站上的所有信息,甚至还买了 Vittorio 的血腥书!我简直不敢相信这么简单的场景根本行不通!
  • @BjørnF.Rasmussen 我已在github.com/Azure-Samples/… 上完整记录了我的麻烦,它与您的非常相似。如果你想联系我,也许我们可以就这个问题交换一些想法。谢谢。
  • 感谢您的意见@ShailenSukul。由于这些问题,我现在放弃了单页应用程序方法,而是使用 SignalR 开发 MVC6 架构。如果你让它工作,请告诉我:)

标签: adal.js


【解决方案1】:

好的,我一直在努力解决这个问题。尝试使我的 ADAL.js SPA 应用程序(无角度)成功地将跨域 XHR 请求发送到我宝贵的支持 CORS 的 Web API。

This sample app,像我这样的新手都在使用,有这个问题:它的 API 和 SPA 都来自同一个域 - 并且只需要一个 AD 租户应用程序注册。这只会在需要将事物拆分成单独的部分时才会混淆。

因此,开箱即用的样本有这个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(由于您在此处列出的代码,WebAPI 接受该令牌),而不是使用它来获取此应用程序的访问令牌。
【解决方案2】:

ADAL.js 确实获得了除 id_token 之外的 access_token,用于调用在不同域上运行的受 Azure AD 保护的 API。 最初,在登录期间,它只需要 id_token。该令牌具有访问同域资源的权限。 但是,在调用在不同域中运行的 API 时,adal 拦截器会检查 API URL 是否配置为 adal.init() 中的端点。

只有这样,访问令牌才会被请求的资源调用。它还需要在 AAD 中配置 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
    );
  1. 授予 Azure AD 中的 SPA 应用程序访问 API 应用程序的权限:

您可以参考此链接了解详情:ADAL.js deep dive

【讨论】:

  • 感谢您的反馈。这似乎与我尝试过的相同,但也许它在更新版本的 adal.js 中工作。我计划明年年初再试一次
  • 有人可以更详细地解释一下 App ID URI 部分吗?我已经让 SPA 应用程序工作并进行身份验证,但是当我尝试对 Web api 进行身份验证时,它不起作用.. 当我查看来自我的 angular 2 应用程序的 http 标头时,我看不到任何带有身份验证信息的标头.这让我相信我没有给它正确的端点信息。如果我的根位置是 blabla.azurewebsites.net 我的应用程序 id uri 也可以是 blabla.azurewebsites.net 吗?
【解决方案3】:

您需要让您的 Web API 了解您的客户端应用程序。从您的客户端向 API 添加委派权限是不够的。

要让 API 客户端知道,请转到 Azure 管理门户,下载 API 的清单并将您的客户端应用程序的 ClientID 添加到“knownClientApplications”列表中。

要允许隐式流,您还需要在清单中将“oauth2AllowImplicitFlow”设置为 true。

将清单上传回 API 应用程序。

【讨论】:

  • 这不是使用 adal.net 的要求,因为我有使用 adal.net 和模拟 API 的工作解决方案。这是对 adal.js 的特殊要求吗?我也试过这个,但是没有用:(
  • @BjørnF.Rasmussen 我尝试在这个 GitHub 线程中提出类似的问题,我得到了一些反馈,我尝试了,但它对我不起作用。检查它是否适合您:github.com/Azure-Samples/…
【解决方案4】:

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

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

SPAs Azure AD 应用程序具有访问外部 APIs Azure AD 应用程序的委派权限。

SPA(基于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
    );

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

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

【讨论】:

  • EmployeeController 是否与 Angular SPA 在同一台机器上? IE。都在同一个 MVC 项目中?
  • 是的,很抱歉没有澄清这一点。我有一个 SPA 解决方案,它包含前端 javascript 以及 EmployeeController。现在,EmployeeController 用于调用真正的逻辑所在的实际后端 Web API。
  • 我明白了。因此,您无法让 adal.js 直接针对外部 api 使用模拟。 EmployeeController 是否作为此限制的解决方法引入?可能是我们在启动项目时最终得到了类似的解决方案(它被推迟了):)
  • 啊!该死,那我误会了。是的,正如您所说,这是一种解决方法。我无法直接从 javascript 与外部 API 对话。很抱歉造成混乱。
猜你喜欢
  • 2017-12-16
  • 1970-01-01
  • 1970-01-01
  • 2017-04-23
  • 2017-12-28
  • 1970-01-01
  • 2019-09-27
  • 1970-01-01
  • 2021-05-04
相关资源
最近更新 更多