Scenario:
I have two ASP.NET web applications hosted separately on Windows Azure and both associated to the same Azure Active Directory tenant:
An MVC app with an AngularJS SPA frontend and the adal.js library for handling Azure AD authentication on the client.
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
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.
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