9

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

4

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 
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks for the response. You have an earlier version of the Microsoft.Owin.Cors NuGet package (I'm using 3.0.0) and are using the tenant ID instead of tenant name for the Owin and adal.js initialization. Other than that it's basically identical. Hoping against hope, I swapped out to the earlier package and tried using the tenant ID. No dice unfortunately. Can you post the request and response headers for an OPTIONS preflight request executed from your working example? I'd be curious to see how, if at all, they differ.
I added example for option request. Do you receive errors with 302? It may be related to permission setup for the apps. I also updated working sample.
Thanks for posting the headers. Interesting. And, yes, I have configured permissions in both apps for Azure AD access. I've updated the question adding that piece into the "What I've tried" section.
Your preflight response is trying to redirect to login. I think owin middleware is not accepting the token and redirecting to login. You can use my cors sample and change the Web.config to try your settings. I provided github repo link above. audience will be the resource you are targeting from web app with angular framework.
It doesn't look like Owin is getting the token on the preflights. Neither of the posted OPTIONS request headers shows an Authorization header with the Bearer token and Chrome dev tools doesn't show any request cookies get sent either. I've also looked at the relevant source code from Katana and Web API. It looks like only the Origin, Method, and Headers are examined during preflight requests.
|
3

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.

5 Comments

Did you ever resolve this issue? I am stuck on the 302 and tried everything possible to get around it. I've deleted everything and started over multiple times. Any help you can provide would be greatly appreciated.
@IsaacLevin, the 302 was resolved as described above. The subsequent 405 was also resolved not too long after, but I honestly don't remember what the solution for it was.
Any chance you can provide your solution in Git as well as screens of your Azure screen? I understand if that is too much to ask for, but I am at my wits end here :)
@IsaacLevin, not without violating NDAs and other contractual obligations entered into with my employer. :)
oh come on man.... you think your job is more important than the greater good? facist ;) I moved on to just having the client in the same app service and as the API and it worked fine. not ideal but it works

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.