Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Scenario:

I have two ASP.NET web applications hosted separately on Windows Azure and both associated to the same Azure Active Directory tenant:

  1. An MVC app with an AngularJS SPA frontend and the adal.js library for handling Azure AD authentication on the client.

  2. Web API with Microsoft OWIN middleware for handling Azure AD authentication on the server.

Problem:

When angular bootstraps the client app, the page loads correctly after going through the oauth redirects to the proper Identity Authority, and the adal.js library correctly retrieves and stores different tokens for each application (verified by inspecting the Resources/Session-Storage tab in Chrome dev tools). But when the client app tries to access or update any data in the API, the CORS preflight requests are responding with 302 redirects to the Identity Authority which results in the following error in the Console:

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.

Example headers (anonymized):

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

What I've done/tried

  1. Ensured the Azure AD tenant allows OAuth2 implicit flow as described here and elsewhere.
  2. Ensured that the API exposes access permissions and that the MVC/SPA registers for access using those exposed permissions.
  3. Explicitly added an OPTIONS verb handler in the API's web.config (see below).
  4. Used various combinations of enabling CORS on the API server, OWIN by itself and also with EnableCorsAttribute (see below).

Questions

Is there any way to have a Web API associated with an Azure AD tenant not redirect on CORS preflight requests? Am I missing some initialization setup in the adal.js library and/or OWIN startup code (see below)? Are there settings in the Azure portal that will allow OPTIONS requests through to the OWIN pipeline?

Relevant code:

adal.js initialization

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 middleware initialization

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 initialization

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 OPTIONS verb handler registration

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

Related resources

At one time or another I've tried just about every imaginable combination of things from the following (and many more) forum and blog posts and github sample code.

  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
like image 782
Bryan Avatar asked Jan 22 '15 20:01

Bryan


1 Answers

I had similar issues to figure out the right packages for that. Only Owin cors is enough to setup.Please check packages for owin.cors first.

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

WebConfig options for handlers:

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

You are doing right with specfiying cors option in owin config.

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

Controller does not need CORS related attributes.

   [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 does not need CORS related entry.

Working example is here:https://github.com/omercs/corsapisample

You can test in your app with the following code:

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;

}]);

Example preflight response:

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
like image 63
Omer Cansizoglu Avatar answered Nov 12 '22 16:11

Omer Cansizoglu