CORS in ConnectSoft Microservice Template¶
Purpose & Overview¶
CORS (Cross-Origin Resource Sharing) is a security mechanism that allows web applications running at one origin (domain) to access resources from another origin. In the ConnectSoft Microservice Template, CORS is configured to enable controlled cross-origin requests for web APIs, allowing frontend applications hosted on different domains to interact with the microservice.
Why CORS?¶
CORS is essential for modern web applications:
- Same-Origin Policy: Browsers enforce same-origin policy by default, blocking cross-origin requests
- Frontend-Backend Separation: Frontend applications often run on different domains/ports than APIs
- Security Control: CORS provides fine-grained control over which origins can access your API
- Development Support: Enables local development with frontend on different ports
- API Access: Allows external applications to consume your API securely
- SPA Support: Single-page applications (SPAs) often require cross-origin requests
CORS Philosophy
CORS is a security feature that must be configured carefully. The template provides sensible defaults for development while encouraging explicit configuration for production. Always specify allowed origins explicitly rather than allowing all origins, and use credentials only when necessary.
Architecture Overview¶
CORS in the Request Pipeline¶
Browser Request
↓
CORS Middleware (UseCors)
├── Check Origin
├── Validate Method
├── Validate Headers
├── Check Credentials
└── Send CORS Headers
↓
Routing Middleware
↓
Controller/Endpoint
↓
Response with CORS Headers
CORS Components¶
CorsExtensions.cs
├── AddCors() - Service Registration
│ ├── Default Policy
│ │ ├── WithOrigins (localhost)
│ │ ├── AllowAnyHeader
│ │ ├── AllowAnyMethod
│ │ └── AllowCredentials
│ └── Named Policies
│ └── AllowAny Policy
└── UseCors() - Middleware
CorsPolicyName.cs
└── Policy Name Constants
Service Registration¶
AddCors Extension¶
CORS is registered via AddCors():
Implementation:
// CorsExtensions.cs
public static IServiceCollection AddCors(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
return services.AddCors(
options =>
{
// Default policy
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins(
"http://localhost:62111",
"https://localhost:7279")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
// Named policy: AllowAny
options.AddPolicy(
CorsPolicyName.AllowAny,
policyBuilder => policyBuilder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
}
CORS Policies¶
Default Policy¶
Configuration:
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins(
"http://localhost:62111",
"https://localhost:7279")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
Characteristics: - Origins: Specific localhost origins (development) - Headers: Any header allowed - Methods: Any HTTP method allowed - Credentials: Credentials allowed (cookies, authorization headers)
Use Case: Default policy for development and local testing
Named Policy: AllowAny¶
Policy Name:
// CorsPolicyName.cs
public static class CorsPolicyName
{
public const string AllowAny = nameof(AllowAny);
}
Configuration:
options.AddPolicy(
CorsPolicyName.AllowAny,
policyBuilder => policyBuilder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
Characteristics:
- Origins: Any origin allowed
- Headers: Any header allowed
- Methods: Any HTTP method allowed
- Credentials: Not allowed (incompatible with AllowAnyOrigin)
Use Case: Public APIs that don't require credentials
AllowAnyOrigin Security
AllowAnyOrigin() is incompatible with AllowCredentials(). Use AllowAnyOrigin() only for public APIs that don't require authentication. For APIs that require credentials, always specify explicit origins using WithOrigins().
Middleware Configuration¶
UseCors Middleware¶
Pipeline Position:
Placement:
// Middleware order:
application.UseForwardedHeaders();
application.UseMicroserviceSerilogRequestLogging();
application.UseDeveloperExceptionPage(); // or UseHsts()
application.UseConversationId();
application.UseMicroserviceHttpLogging(configuration);
application.UseLatencyTelemetryCollection();
application.UseDefaultFiles();
application.UseStaticFiles();
application.UseWebSockets();
#if UseRestApi
application.UseWebApiCommunication();
#endif
#if CORS
application.UseCors(); // ← CORS middleware
#endif
application.UseHttpsRedirection();
application.UseRouting();
// ... rest of middleware
Important: CORS middleware must be placed: - After static files and WebSockets - Before routing and authentication - Before HTTPS redirection
Using Default Policy¶
Automatic Application:
The default policy is automatically applied to all requests when UseCors() is called without a policy name.
Using Named Policy¶
Specific Policy:
Per-Controller:
[ApiController]
[EnableCors(CorsPolicyName.AllowAny)]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
// Controller actions
}
Per-Action:
Disable CORS:
[ApiController]
[DisableCors]
[Route("api/[controller]")]
public class InternalController : ControllerBase
{
// CORS disabled for this controller
}
CORS Configuration Options¶
Origin Configuration¶
Specific Origins:
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins(
"https://example.com",
"https://www.example.com",
"https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod());
Any Origin (not recommended for production):
options.AddPolicy(
"AllowAny",
policyBuilder => policyBuilder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
Origin Patterns (advanced):
options.AddPolicy(
"AllowSubdomains",
policyBuilder => policyBuilder
.SetIsOriginAllowed(origin =>
new Uri(origin).Host.EndsWith(".example.com"))
.AllowAnyMethod()
.AllowAnyHeader());
Method Configuration¶
Allow Any Method:
Specific Methods:
Common HTTP Methods:
- GET - Retrieve resource
- POST - Create resource
- PUT - Update resource
- PATCH - Partial update
- DELETE - Delete resource
- OPTIONS - Preflight request (automatically handled)
Header Configuration¶
Allow Any Header:
Specific Headers:
Exposed Headers (headers exposed to client):
Common Headers:
- Content-Type - Request body type
- Authorization - Authentication token
- Accept - Response content type preference
- X-Requested-With - AJAX request indicator
Credentials Configuration¶
Allow Credentials (cookies, authorization headers):
Requirements:
- Cannot be used with AllowAnyOrigin()
- Must specify explicit origins with WithOrigins()
- Browser will include cookies and authorization headers in requests
Example:
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins("https://example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()); // ✅ OK - explicit origin
options.AddPolicy(
"Invalid",
policyBuilder => policyBuilder
.AllowAnyOrigin()
.AllowCredentials()); // ❌ ERROR - incompatible
Preflight Configuration¶
Preflight Duration:
Controls how long browsers cache preflight (OPTIONS) responses.
Default: No cache (browser sends preflight for every request)
Benefits of Caching: - Performance: Fewer preflight requests - Reduced Load: Less server processing
Security Consideration: Shorter cache times provide more security but less performance.
Custom CORS Policies¶
Creating Custom Policies¶
Production Policy:
options.AddPolicy(
"Production",
policyBuilder => policyBuilder
.WithOrigins(
"https://app.production.com",
"https://admin.production.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromMinutes(10)));
Development Policy:
options.AddPolicy(
"Development",
policyBuilder => policyBuilder
.WithOrigins(
"http://localhost:3000",
"http://localhost:4200",
"http://localhost:5173",
"https://localhost:7279")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
Public API Policy:
options.AddPolicy(
"PublicAPI",
policyBuilder => policyBuilder
.AllowAnyOrigin()
.WithMethods("GET", "POST")
.WithHeaders("Content-Type")
.SetPreflightMaxAge(TimeSpan.FromHours(1)));
Environment-Based Policies¶
Conditional Configuration:
public static IServiceCollection AddCors(
this IServiceCollection services,
IWebHostEnvironment environment)
{
return services.AddCors(options =>
{
if (environment.IsDevelopment())
{
// Development: Allow localhost origins
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins(
"http://localhost:3000",
"https://localhost:7279")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
}
else
{
// Production: Specific origins only
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins(
"https://app.production.com",
"https://admin.production.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.AllowCredentials());
}
});
}
Configuration-Based CORS¶
Using Configuration¶
appsettings.json:
{
"Cors": {
"AllowedOrigins": [
"https://example.com",
"https://www.example.com"
],
"AllowedMethods": [
"GET",
"POST",
"PUT",
"DELETE"
],
"AllowedHeaders": [
"Content-Type",
"Authorization"
],
"AllowCredentials": true,
"PreflightMaxAge": "00:10:00"
}
}
Configuration-Based Setup:
public static IServiceCollection AddCors(
this IServiceCollection services,
IConfiguration configuration)
{
var corsOptions = configuration.GetSection("Cors").Get<CorsOptions>();
return services.AddCors(options =>
{
options.AddDefaultPolicy(policyBuilder =>
{
if (corsOptions?.AllowedOrigins != null)
{
policyBuilder.WithOrigins(corsOptions.AllowedOrigins);
}
if (corsOptions?.AllowedMethods != null)
{
policyBuilder.WithMethods(corsOptions.AllowedMethods);
}
else
{
policyBuilder.AllowAnyMethod();
}
if (corsOptions?.AllowedHeaders != null)
{
policyBuilder.WithHeaders(corsOptions.AllowedHeaders);
}
else
{
policyBuilder.AllowAnyHeader();
}
if (corsOptions?.AllowCredentials == true)
{
policyBuilder.AllowCredentials();
}
if (corsOptions?.PreflightMaxAge != null)
{
policyBuilder.SetPreflightMaxAge(corsOptions.PreflightMaxAge);
}
});
});
}
CORS Headers¶
Request Headers¶
Origin Header (sent by browser):
Access-Control-Request-Method (preflight):
Access-Control-Request-Headers (preflight):
Response Headers¶
Access-Control-Allow-Origin:
Access-Control-Allow-Methods:
Access-Control-Allow-Headers:
Access-Control-Allow-Credentials:
Access-Control-Expose-Headers:
Access-Control-Max-Age (preflight cache):
Preflight Requests¶
What Are Preflight Requests?¶
Preflight requests are OPTIONS requests sent by browsers before certain cross-origin requests to check if the actual request is allowed.
Triggered When: - Method is not simple (GET, HEAD, POST) - Custom headers are present - Content-Type is not simple (application/json, etc.)
Preflight Request Flow¶
1. Browser sends OPTIONS request
OPTIONS /api/data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
2. Server responds with CORS headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type
3. Browser sends actual request (if preflight succeeded)
POST /api/data HTTP/1.1
Origin: https://example.com
Content-Type: application/json
Handling Preflight¶
Automatic Handling: ASP.NET Core CORS middleware automatically handles preflight requests.
Manual Handling (if needed):
Security Considerations¶
Origin Validation¶
Always Validate Origins:
// ✅ GOOD - Explicit origins
.WithOrigins("https://example.com", "https://app.example.com")
// ❌ BAD - Allow any origin with credentials
.AllowAnyOrigin().AllowCredentials() // ❌ Invalid combination
Credentials Security¶
Use Credentials Only When Necessary:
// ✅ GOOD - Credentials with explicit origins
.WithOrigins("https://example.com")
.AllowCredentials()
// ❌ BAD - Credentials with any origin
.AllowAnyOrigin()
.AllowCredentials() // ❌ Runtime exception
HTTPS in Production¶
Always Use HTTPS Origins in Production:
// ✅ GOOD - HTTPS origins
.WithOrigins(
"https://app.production.com",
"https://admin.production.com")
// ❌ BAD - HTTP origins in production
.WithOrigins("http://app.production.com") // ❌ Insecure
Method Restrictions¶
Allow Only Necessary Methods:
// ✅ GOOD - Specific methods
.WithMethods("GET", "POST")
// ❌ BAD - All methods when not needed
.AllowAnyMethod() // ❌ Too permissive
Header Restrictions¶
Allow Only Necessary Headers:
// ✅ GOOD - Specific headers
.WithHeaders("Content-Type", "Authorization")
// ❌ BAD - All headers when not needed
.AllowAnyHeader() // ❌ Too permissive
Best Practices¶
Do's¶
-
Specify Explicit Origins
-
Use Environment-Based Configuration
-
Restrict Methods and Headers
-
Use Credentials with Explicit Origins
-
Cache Preflight Responses
-
Use Named Policies for Different Scenarios
Don'ts¶
-
Don't Use AllowAnyOrigin in Production
-
Don't Combine AllowAnyOrigin with Credentials
-
Don't Use HTTP Origins in Production
-
Don't Allow All Methods Unnecessarily
-
Don't Allow All Headers Unnecessarily
Troubleshooting¶
Issue: CORS Errors in Browser¶
Symptoms: Browser console shows CORS errors like:
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
Solutions:
1. Verify CORS middleware is registered: services.AddCors()
2. Verify CORS middleware is in pipeline: application.UseCors()
3. Check middleware order (CORS before routing)
4. Verify origin is in allowed list
5. Check if credentials are required (if so, origin must be explicit)
6. Review browser console for specific error
Issue: Preflight Requests Failing¶
Symptoms: OPTIONS requests return 405 or 404.
Solutions: 1. Verify CORS middleware handles OPTIONS automatically 2. Check routing configuration 3. Verify middleware order 4. Check if endpoint supports OPTIONS method
Issue: Credentials Not Working¶
Symptoms: Cookies or authorization headers not sent.
Solutions:
1. Verify AllowCredentials() is configured
2. Check origin is explicit (not AllowAnyOrigin())
3. Verify client sends credentials: 'include' in fetch
4. Check cookies are not HttpOnly (if needed)
5. Verify SameSite cookie attribute
Issue: CORS Works Locally But Not in Production¶
Symptoms: CORS works in development but fails in production.
Solutions: 1. Check production origins are in allowed list 2. Verify HTTPS is used in production 3. Check environment-specific configuration 4. Verify reverse proxy (if any) passes CORS headers 5. Check firewall or security rules
Issue: Multiple Origins Not Working¶
Symptoms: Some origins work, others don't.
Solutions:
1. Verify all origins are in WithOrigins() list
2. Check origin format (must include protocol: https://)
3. Verify no trailing slashes in origins
4. Check case sensitivity (origins are case-sensitive)
5. Review browser console for specific error
Integration with Other Features¶
CORS + Authentication¶
Working Together:
options.AddDefaultPolicy(
policyBuilder => policyBuilder
.WithOrigins("https://example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()); // Enables sending auth headers
Middleware Order:
application.UseCors(); // Before authentication
application.UseAuthentication();
application.UseAuthorization();
CORS + SignalR¶
SignalR CORS Configuration:
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
});
services.AddCors(options =>
{
options.AddPolicy("SignalRCors",
policy => policy
.WithOrigins("https://example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
});
// In middleware
application.UseCors("SignalRCors");
// SignalR endpoint
endpoints.MapHub<MyHub>("/hubs/myhub")
.RequireCors("SignalRCors");
CORS + Swagger¶
Swagger CORS Configuration:
services.AddCors(options =>
{
options.AddPolicy("SwaggerCors",
policy => policy
.WithOrigins("https://swagger.example.com")
.AllowAnyHeader()
.AllowAnyMethod());
});
application.UseCors("SwaggerCors");
application.UseSwagger();
application.UseSwaggerUI();
Testing CORS¶
Manual Testing¶
Browser Console:
// Test CORS request
fetch('https://api.example.com/api/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include' // If credentials required
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS Error:', error));
Unit Testing¶
Test CORS Middleware:
[TestMethod]
public async Task CorsMiddleware_AddsCorsHeaders()
{
// Arrange
var services = new ServiceCollection();
services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy.WithOrigins("https://example.com")
.AllowAnyMethod()
.AllowAnyHeader());
});
var provider = services.BuildServiceProvider();
var app = new ApplicationBuilder(provider);
app.UseCors();
// Act
var context = new DefaultHttpContext();
context.Request.Headers.Add("Origin", "https://example.com");
context.Request.Method = "GET";
// Assert
// Verify CORS headers are set
}
Summary¶
CORS in the ConnectSoft Microservice Template provides:
- ✅ Secure Cross-Origin Access: Controlled access from different origins
- ✅ Development Support: Default policy for localhost development
- ✅ Flexible Configuration: Multiple policies for different scenarios
- ✅ Security Best Practices: Explicit origins, method/header restrictions
- ✅ Credentials Support: Secure credential handling with explicit origins
- ✅ Preflight Handling: Automatic preflight request handling
- ✅ Environment Awareness: Different policies for development and production
- ✅ Integration Ready: Works with authentication, SignalR, Swagger
By following these patterns, teams can:
- Enable Cross-Origin Requests: Allow frontend applications to access APIs
- Maintain Security: Control which origins can access your API
- Support Development: Easy localhost development with different ports
- Deploy Safely: Production-ready CORS configuration
- Handle Credentials: Secure authentication with cross-origin requests
- Optimize Performance: Cache preflight responses for better performance
CORS configuration is essential for modern web applications that separate frontend and backend, enabling secure and controlled cross-origin access while maintaining security best practices.