Skip to content

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():

// MicroserviceRegistrationExtensions.cs
#if CORS
    services.AddCors();
#endif

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:

// MicroserviceRegistrationExtensions.cs
#if CORS
    application.UseCors();
#endif

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:

application.UseCors(); // Uses default policy

The default policy is automatically applied to all requests when UseCors() is called without a policy name.

Using Named Policy

Specific Policy:

application.UseCors(CorsPolicyName.AllowAny);

Per-Controller:

[ApiController]
[EnableCors(CorsPolicyName.AllowAny)]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
    // Controller actions
}

Per-Action:

[HttpGet]
[EnableCors(CorsPolicyName.AllowAny)]
public IActionResult Get()
{
    return Ok();
}

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:

.AllowAnyMethod()

Specific Methods:

.WithMethods("GET", "POST", "PUT", "DELETE")

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:

.AllowAnyHeader()

Specific Headers:

.WithHeaders("Content-Type", "Authorization", "X-Requested-With")

Exposed Headers (headers exposed to client):

.WithExposedHeaders("X-Total-Count", "X-Page-Number")

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):

.AllowCredentials()

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:

.SetPreflightMaxAge(TimeSpan.FromHours(1))

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):

Origin: https://example.com

Access-Control-Request-Method (preflight):

Access-Control-Request-Method: POST

Access-Control-Request-Headers (preflight):

Access-Control-Request-Headers: Content-Type, Authorization

Response Headers

Access-Control-Allow-Origin:

Access-Control-Allow-Origin: https://example.com
// or
Access-Control-Allow-Origin: *

Access-Control-Allow-Methods:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE

Access-Control-Allow-Headers:

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Allow-Credentials:

Access-Control-Allow-Credentials: true

Access-Control-Expose-Headers:

Access-Control-Expose-Headers: X-Total-Count, X-Page-Number

Access-Control-Max-Age (preflight cache):

Access-Control-Max-Age: 3600

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):

[HttpOptions]
public IActionResult Options()
{
    return Ok();
}

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

  1. Specify Explicit Origins

    // ✅ GOOD - Explicit origins
    .WithOrigins("https://example.com", "https://app.example.com")
    
    // ❌ BAD - Any origin
    .AllowAnyOrigin()
    

  2. Use Environment-Based Configuration

    // ✅ GOOD - Different policies per environment
    if (environment.IsDevelopment())
    {
        // Development policy
    }
    else
    {
        // Production policy
    }
    

  3. Restrict Methods and Headers

    // ✅ GOOD - Specific methods and headers
    .WithMethods("GET", "POST")
    .WithHeaders("Content-Type", "Authorization")
    
    // ❌ BAD - Allow all
    .AllowAnyMethod()
    .AllowAnyHeader()
    

  4. Use Credentials with Explicit Origins

    // ✅ GOOD - Credentials with explicit origins
    .WithOrigins("https://example.com")
    .AllowCredentials()
    
    // ❌ BAD - Credentials with any origin
    .AllowAnyOrigin()
    .AllowCredentials() // ❌ Invalid
    

  5. Cache Preflight Responses

    // ✅ GOOD - Cache preflight
    .SetPreflightMaxAge(TimeSpan.FromMinutes(10))
    

  6. Use Named Policies for Different Scenarios

    // ✅ GOOD - Named policies
    options.AddPolicy("PublicAPI", ...);
    options.AddPolicy("PrivateAPI", ...);
    

Don'ts

  1. Don't Use AllowAnyOrigin in Production

    // ❌ BAD - Security risk
    .AllowAnyOrigin()
    
    // ✅ GOOD - Explicit origins
    .WithOrigins("https://example.com")
    

  2. Don't Combine AllowAnyOrigin with Credentials

    // ❌ BAD - Runtime exception
    .AllowAnyOrigin()
    .AllowCredentials() // ❌ Throws exception
    

  3. Don't Use HTTP Origins in Production

    // ❌ BAD - Insecure
    .WithOrigins("http://app.production.com")
    
    // ✅ GOOD - HTTPS only
    .WithOrigins("https://app.production.com")
    

  4. Don't Allow All Methods Unnecessarily

    // ❌ BAD - Too permissive
    .AllowAnyMethod()
    
    // ✅ GOOD - Specific methods
    .WithMethods("GET", "POST")
    

  5. Don't Allow All Headers Unnecessarily

    // ❌ BAD - Too permissive
    .AllowAnyHeader()
    
    // ✅ GOOD - Specific headers
    .WithHeaders("Content-Type", "Authorization")
    

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.