I’m working on an project for a customer which implements a number of application programming Interfaces(API) for a Single Page Application(SPA) and other clients. We are using entity tags (ETags) for versioning and the front end developers found the couldn’t access them from javascipt running in mainstream browser clients (June 2018).
The problems was understanding how Cross-Origin Resource Sharing (CORS) worked and how it interacted with our security model (API key and OAuth2.0 depending on the client)
In our scenario we first found the pre-flight check wasn’t working because in the HyperText Transfer Protocol (HTTP) OPTIONS method our X-API-KEY check was failing
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... Access-Control-Request-Headers: x-api-key Access-Control-Request-Method: GET Accept-Encoding: gzip, deflate Content-Length: 0 Host: xyz.azurewebsites.net Connection: Keep-Alive Pragma: no-cache HTTP/1.1 400 Bad Request Transfer-Encoding: chunked Server: Kestrel X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 05:48:30 GMT 13 API Key is invalid. 0
So I disabled X-API-KEY validation in startup.cs
public async Task Invoke(HttpContext context) { if (context.Request.Method == "OPTIONS") { await this.next.Invoke(context); return; } var claims = new List(); …
OPTIONS then worked
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... Access-Control-Request-Headers: x-api-key Access-Control-Request-Method: GET Accept-Encoding: gzip, deflate Content-Length: 0 Host: xyz.azurewebsites.net Connection: Keep-Alive Pragma: no-cache HTTP/1.1 404 Not Found Server: Kestrel X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 05:52:20 GMT Content-Length: 0
I then turned on CORS allowing pretty much anything
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } TelemetryConfiguration.Active.InstrumentationKey = this.configuration.GetSection("ApplicationInsights").GetSection("InstrumentationKey").Value; loggerFactory.AddLog4Net(); this.log.Info("Startup.Configure called"); app.ApplyUserKeyValidation(); app.UseCors("CorsPolicy"); app.UseMvc(); } }
OPTIONS then worked
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... Access-Control-Request-Headers: x-api-key Access-Control-Request-Method: GET Accept-Encoding: gzip, deflate Content-Length: 0 Host: xyz.azurewebsites.net Connection: Keep-Alive Pragma: no-cache HTTP/1.1 204 No Content Vary: Origin Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: x-api-key Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 05:57:33 GMT
GET then worked
GET http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... X-API-KEY: ABCDEFGHIJKLMNOPQRSTUVWXYZ Accept-Language: en-NZ Accept-Encoding: gzip, deflate If-None-Match: 00-00-00-00-00-00-00-76 Host: xyz.azurewebsites.net Connection: Keep-Alive HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 ETag: 00-00-00-00-00-00-00-76 Vary: Origin,Accept-Encoding Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 05:57:34 GMT Content-Length: 2216 [{"...."}}
But HEAD didn’t work
HEAD http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... X-API-KEY: ABCDEFGHIJKLMNOPQRSTUVWXYZ Accept-Language: en-NZ Accept-Encoding: gzip, deflate If-None-Match: 00-00-00-00-00-00-00-76 Host: xyz.azurewebsites.net Connection: Keep-Alive HTTP/1.1 400 Bad Request Content-Length: 0 Vary: Origin Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 05:59:55 GMT
From the Application Insights logging and RestTest client (which I ran locally and remotely) I could see that the client side code couldn’t access the value of our eTag. It had to be “exposed”
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .WithExposedHeaders("etag") .AllowCredentials() ); }); services.AddMvc(); } ...
GET then worked
GET http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... X-API-KEY: ABCDEFGHIJKLMNOPQRSTUVWXYZ ETag: 00-00-00-00-00-00-00-76 Accept-Language: en-NZ Accept-Encoding: gzip, deflate If-None-Match: 00-00-00-00-00-00-00-76 Host: xyz.azurewebsites.net Connection: Keep-Alive HTTP/1.1 200 OK Transfer-Encoding: chunked Content-Type: application/json; charset=utf-8 Content-Encoding: gzip ETag: 00-00-00-00-00-00-00-76 Vary: Origin,Accept-Encoding Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 07:53:41 GMT [{"...."}}
HEAD then worked
OPTIONS http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... Access-Control-Request-Headers: x-api-key,etag Access-Control-Request-Method: HEAD Accept-Encoding: gzip, deflate Content-Length: 0 Host: xyz.azurewebsites.net Connection: Keep-Alive Pragma: no-cache HTTP/1.1 204 No Content Vary: Origin Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: x-api-key,etag Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 07:57:31 GMT HEAD http://xyz.azurewebsites.net/api/portfolio HTTP/1.1 ... X-API-KEY: ABCDEFGHIJKLMNOPQRSTUVWXYZ ETag: 00-00-00-00-00-00-00-76 Accept-Language: en-NZ Accept-Encoding: gzip, deflate Host: xyz.azurewebsites.net Connection: Keep-Alive Pragma: no-cache HTTP/1.1 304 Not Modified Vary: Origin Server: Kestrel Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: file:// X-Powered-By: ASP.NET ... Date: Sun, 24 Jun 2018 07:57:31 GMT
I had some oddness with releasing code updates which I think was down to caching of pre-flight request responses.
Next steps tidy up the headers etc. and lock the CORS configuration down to expose the minimum necessary required for the application to work.