CORS 预检请求在 Azure 托管 Web 中以 302 重定向响应 API

CORS preflight request responds with 302 redirect in Azure hosted Web API

场景:

我有两个 ASP.NET Web 应用程序分别托管在 Windows Azure 上,并且都关联到同一个 Azure Active Directory 租户:

  1. 具有 AngularJS SPA 前端和 adal.js 库的 MVC 应用程序,用于在客户端处理 Azure AD 身份验证。

  2. Web API 带有 Microsoft OWIN 中间件,用于处理服务器上的 Azure AD 身份验证。

问题:

当 angular 引导客户端应用程序时,页面在通过 oauth 重定向到正确的身份授权后正确加载,并且 adal.js 库为每个应用程序正确检索和存储不同的令牌 (通过检查 Chrome 开发工具中的 Resources/Session-Storage 选项卡进行验证)。 但是当客户端应用程序尝试访问或更新 API 中的任何数据时, CORS 预检请求以 302 重定向响应身份授权,这导致控制台中出现以下错误:

XMLHttpRequest cannot load https://webapi.azurewebsites.net/api/items. The request was redirected to 'https://login.windows.net/{authority-guid}/oauth2/authorize?response_type=id_token&redirect_uri=....etc..etc..', which is disallowed for cross-origin requests that require preflight.

示例headers(匿名):

Request
OPTIONS /api/items HTTP/1.1
Host: webapi.azurewebsites.net
Connection: keep-alive
Access-Control-Request-Method: GET
Access-Control-Request-Headers: accept, authorization
Origin: https://mvcapp.azurewebsites.net
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36
Accept: */*
Referer: https://mvcapp.azurewebsites.net/

Response
HTTP/1.1 302 Found
Content-Length: 504
Location: https://login.windows.net/{authority-guid}/oauth2/authorize?response_type=id_token&redirect_uri=https%3A%2F%2F....etc..etc.%2F&client_id={api-guid}&scope=openid+profile+email&response_mode=form_post&state=...etc...
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=4f51...snip....redact....db6d;Path=/;Domain=webapi.azurewebsites.net

我有什么done/tried

  1. 确保 Azure AD 租户允许 OAuth2 隐式流程,如 here 和其他地方所述。
  2. 确保 API exposes access permissions and that the MVC/SPA registers for access 使用那些暴露的权限。
  3. 在 API 的 web.config 中明确添加了一个 OPTIONS 动词处理程序(见下文)。
  4. 在 API 服务器上使用了启用 CORS 的各种组合,OWIN 本身以及 EnableCorsAttribute(见下文)。

问题

有什么方法可以让与 Azure AD 租户关联的 Web API 不在 CORS 预检请求上重定向? 我是否缺少 adal.js 库 and/or OWIN 启动代码中的一些初始化设置(见下文)? Azure 门户中是否有允许 OPTIONS 请求通过 OWIN 管道的设置?

相关代码:

adal.js初始化

angular.module("myApp", ["ngRoute", "AdalAngular"])

.config(["$routeProvider", "$locationProvider", "$httpProvider", "adalAuthenticationServiceProvider",
    function ($routeProvider, $locationProvider, $httpProvider, adalProvider) {

        $routeProvider.when("/", { // other routes omitted for brevity
            templateUrl: "/content/views/home.html",
            requireADLogin: true // restrict to validated users in the Azure AD tenant
        });

        // CORS support (I've tried with and without this line)
        $httpProvider.defaults.withCredentials = true;

        adalProvider.init({
            tenant: "contoso.onmicrosoft.com",
            clientId: "11111111-aaaa-2222-bbbb-3333cccc4444", // Azure id of the web app
            endpoints: {
                // URL and Azure id of the web api
                "https://webapi.azurewebsites.net/": "99999999-zzzz-8888-yyyy-7777xxxx6666"
            }
        }, $httpProvider);
    }
]);

OWIN 中间件初始化

public void ConfigureAuth(IAppBuilder app)
{
    // I've tried with and without the below line and also by passing
    // in a more restrictive and explicit custom CorsOptions object
    app.UseCors(CorsOptions.AllowAll);

    app.UseWindowsAzureActiveDirectoryBearerAuthentication(
        new WindowsAzureActiveDirectoryBearerAuthenticationOptions
        {
            TokenValidationParameters = new TokenValidationParameters
            {
                // Azure id of the Web API, also tried the client app id
                ValidAudience = "99999999-zzzz-8888-yyyy-7777xxxx6666"
            },
            Tenant = "contoso.onmicrosoft.com"
        }
    );

    // I've tried with and without this
    app.UseWebApi(GlobalConfiguration.Configuration);
}

WebApiConfig 初始化

public static void Register(HttpConfiguration config)
{
    // I've tried with and without this and also using both this
    // and the OWIN CORS setup above. Decorating the ApiControllers
    // or specific Action methods with a similar EnableCors attribute
    // also doesn't work.
    var cors = new EnableCorsAttribute("https://mvcapp.azurewebsites.net", "*", "*")
    {
        cors.SupportsCredentials = true // tried with and without
    };
    config.EnableCors(cors);

    // Route registration and other initialization code removed
}

API 选项动词处理程序注册

<system.webServer>
  <handlers>
    <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
    <remove name="OPTIONSVerbHandler" />
    <remove name="TRACEVerbHandler" />
    <add name="OPTIONSHandler" path="*" verb="OPTIONS" modules="IsapiModule" scriptProcessor="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" />
    <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
  </handlers>
</system.webServer>

相关资源

我曾经尝试过以下(以及更多)论坛和博客帖子以及 github 示例代码中几乎所有可以想象的组合。

  1. ADAL JavaScript and AngularJS – Deep Dive
  2. Secure ASP.NET Web API 2 using Azure Active Directory, Owin Middleware, and ADAL
  3. Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
  4. AzureADSamples/SinglePageApp-DotNet (github)
  5. AngularJSCORS (github)
  6. How to make CORS Authentication in WebAPI 2?
  7. AngularJS and OWIN Authentication on WebApi

我有类似的问题来找出合适的包。只有 Owin cors 足以 setup.Please 首先检查 owin.cors 的包。

<package id="Microsoft.Owin" version="3.0.0" targetFramework="net45" />
<package id="Microsoft.Owin.Cors" version="2.1.0" targetFramework="net45" />

处理程序的 WebConfig 选项:

<system.webServer>
<handlers>
  <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
  <remove name="OPTIONSVerbHandler" />
  <remove name="TRACEVerbHandler" />
  <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>

您在 owin 配置中使用 specfiying cors 选项做得很好。

 public void ConfigureAuth(IAppBuilder app)
    {
        app.UseWindowsAzureActiveDirectoryBearerAuthentication(
            new WindowsAzureActiveDirectoryBearerAuthenticationOptions
            {
                Audience = ConfigurationManager.AppSettings["ida:Audience"],
                Tenant = ConfigurationManager.AppSettings["ida:Tenant"]

            });
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
    }

控制器不需要CORS相关属性。

   [Authorize]
public class ContactsController : ApiController
{

    // GET api/<controller>
    public IEnumerable<string> Get()
    {
        return new string[] { "person1", "person2" };
    }

    // GET api/<controller>/5
    public string Get(int id)
    {
        return "person" + id;
    }

WebAPIConfig 不需要 CORS 相关条目。

工作示例在这里:https://github.com/omercs/corsapisample

您可以使用以下代码在您的应用中进行测试:

app.factory('contactService', ['$http', function ($http) {
var serviceFactory = {};

var _getItems = function () {
    $http.defaults.useXDomain = true;
    delete $http.defaults.headers.common['X-Requested-With'];
    return $http.get('http://yourhostedpage/api/contacts');
};

serviceFactory.getItems = _getItems;

return serviceFactory;

}]);

预检响应示例:

Remote Address:127.0.0.1:8888
Request URL:http://localhost:39725/api/contacts
Request Method:OPTIONS
Status Code:200 OK
Request Headersview source
Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:en-US,en;q=0.8
Access-Control-Request-Headers:accept, authorization
Access-Control-Request-Method:GET
Host:localhost:39725
Origin:http://localhost:49726
Proxy-Connection:keep-alive
Referer:http://localhost:49726/myspa.html
User-Agent:Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.99 Safari/537.36
Response Headersview source
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:authorization
Access-Control-Allow-Origin:http://localhost:49726
Content-Length:0
Date:Fri, 23 Jan 2015 01:10:54 GMT
Server:Microsoft-IIS/8.0
X-Powered-By:ASP.NET

已解决(有点)

这似乎是由部署问题引起的,特别是我最初将应用程序发布到 Azure 的方式。这两个应用程序最初都是使用 Windows 身份验证编写的,并部署到标准(即非 Azure)Web 服务器。使用这些技术,应用程序按预期运行。

根据新的业务需求,我一直在努力将它们迁移到 Azure。我遵循的过程是:

  1. Deploy/Publish 这两个应用程序,如最初编写的那样,来自 Visual Studio 2013 直接发布向导到 Azure。当然,正如预期的那样,这失败了,因为他们无法从 Azure 内部与本地 Active Directory 通信。
  2. 逐步更新代码以删除 Windows 身份验证并按照问题末尾所有链接中的详细信息替换为 Azure AD 身份验证。
  3. 手动将应用程序与 Azure AD 租户相关联,以便使用 Azure AD 门户进行身份验证。

在某个时候,我注意到 VS 发布向导的“设置”选项卡上的“启用组织身份验证”选项,并开始使用它来将应用程序与租户相关联。因为我已经手动关联了它们,所以 Visual Studio 最终为每个创建了另一个 Azure AD 应用程序,结果是两个。

最后是这样的:

  1. 删除了使用门户的两个应用程序的所有 Azure 记录,包括 Azure 网站本身和 Azure AD sites/associations。
  2. 将两个应用程序中的所有代码更改还原为原始状态。
  3. 在应用程序中重新实现了 Azure AD 身份验证(OWIN、adal.js 等)。
  4. 是否重新发布了两个应用程序,让 VS 向导处理所有 Azure 关联。
  5. 使用新创建的客户端 ID 更新了 Web.config 文件和 adal.js 初始化。
  6. 再次发布。

现在 OPTIONS 预检请求不再是 302 重定向。

相反,它们现在是 405 Method Not Allowed,与 非常相似。某种程度上的进步。

尽管它仍然不能端到端地工作,但我会留下这个答案(而不是删除问题),以防它可能帮助其他人体验 Azure 302 重定向的 CORS 预检。