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
- Ensured the Azure AD tenant allows OAuth2 implicit flow as described here and elsewhere.
- Ensured that the API exposes access permissions and that the MVC/SPA registers for access using those exposed permissions.
- Explicitly added an OPTIONS verb handler in the API's web.config (see below).
- 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.
- ADAL JavaScript and AngularJS – Deep Dive
- Secure ASP.NET Web API 2 using Azure Active Directory, Owin Middleware, and ADAL
- Token Based Authentication using ASP.NET Web API 2, Owin, and Identity
- AzureADSamples/SinglePageApp-DotNet (github)
- AngularJSCORS (github)
- How to make CORS Authentication in WebAPI 2?
- AngularJS and OWIN Authentication on WebApi