9
votes

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
2

2 Answers

4
votes

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

Solved (sort of)

This appears to have been caused by deployment issues, specifically how I initially published the applications to Azure. Both apps were originally written using Windows Authentication and were deployed to a standard (i.e. non-Azure) web server. Using these technologies the apps worked as expected.

Per new business requirements, I have been working on migrating them to Azure. The process I followed was:

  1. Deploy/Publish both apps, as originally written, from Visual Studio 2013 Publish wizard directly to Azure. Of course this broke, as expected, since they couldn't communicate with the on-premises Active Directory from within Azure.
  2. Gradually update the code to remove Windows Auth and replace with Azure AD auth following the details from all the links at the end of the question.
  3. Manually associated the apps with the Azure AD tenant for authentication using the Azure AD portal.

At some point, I noticed the Enable Organizational Authentication option on the Settings tab of the VS Publish wizard and started using it to associate the apps with the tenant. Because I had already manually associated them, Visual Studio ended up creating another Azure AD app for each resulting in two.

In the end, this is what did:

  1. Deleted all of the Azure records for both applications using the Portal, both the Azure websites themselves and also the Azure AD sites/associations.
  2. Reverted all code changes in both apps back to their original state.
  3. Reimplemented Azure AD auth (OWIN, adal.js, etc) in the apps.
  4. Did a fresh publish of both apps letting the VS Wizard handle all the Azure associations.
  5. Updated the Web.config files and the adal.js initialization with the newly created client IDs.
  6. Published again.

Now the OPTIONS preflight requests are no longer 302 redirects.

Instead they are now 405 Method Not Allowed, very similar to this thread. Progress, of a sort.

Even though it's still not working end-to-end, I'll leave this answer (rather than delete the question) in case it might help others experiencing an Azure 302 redirected CORS preflight.