CORS with Credentials
Introduction
Welcome to the third lesson of the "Securing your REST API application with ASP.NET Core" course! In our previous lessons, we explored the basics of cross-origin resource sharing (CORS) and how to handle preflight requests. Now, we're going to tackle one of the most challenging aspects of modern web development: enabling and customizing CORS with credentials for cross-origin cookie authentication.
This lesson addresses a common real-world scenario where your frontend application runs on one domain (perhaps app.example.com or even a CDN) while your API runs on another (api.example.com). You'll learn how to securely share cookies across these different origins while maintaining strong security posture. This capability is essential for maintaining session state and authenticating users across distributed applications.
By the end of this lesson, you'll understand not just how to implement cross-origin cookie authentication, but also when it's appropriate to use this approach and what security considerations you must address. We'll work through practical code examples that you can immediately apply to your projects. Let's dive in! 🌟
Cookies And Web Authentication
Before we tackle the complexities of cross-origin scenarios, let's establish a solid foundation. Cookies are fundamental to how web applications maintain session state — they're small pieces of data that servers send to browsers, which browsers then store and send back with subsequent requests.
Think of cookies as a claim ticket at a coat check. When you log in (check your coat), the server gives you a unique identifier (the ticket). Every time you make a request (want your coat back), you present this ticket, and the server knows who you are without requiring you to log in again. This mechanism allows users to navigate through your application while remaining authenticated.
In cross-origin scenarios, however, browsers enforce strict security policies about cookie sharing. By default, browsers won't send cookies when making requests to a different origin (domain, port, or protocol). This restriction exists for good reason — imagine if any website could make requests to your banking application using your cookies! Understanding these security boundaries is crucial before we learn how to carefully and safely expand them for legitimate use cases.
Cross-Origin Cookie Authentication
Cross-origin cookie authentication involves using cookies for authentication when your frontend and backend exist on different origins. While this sounds straightforward, browsers block this by default due to serious security concerns.
Consider these security implications that browsers protect against:
-
Cross-site request forgery (CSRF) represents a particularly dangerous threat. Without proper restrictions, a malicious website could make authenticated requests to your API using a user's existing cookies, potentially performing unauthorized actions like transferring funds or changing account settings — all without the user's knowledge.
-
Information leakage poses another risk. Cookies often contain sensitive session data or identifiers that shouldn't be accessible to third-party sites. If every website could read cookies from every other domain, user privacy would be completely compromised.
-
Privacy implications extend beyond security. User tracking across multiple domains could violate privacy expectations and regulations. When we enable cross-origin cookie sharing, we must do so carefully and only with trusted domains.
To address these concerns while enabling legitimate cross-origin scenarios, we need a coordinated approach between server and client. The server must explicitly permit specific origins to receive credentials — via AllowCredentials() in the CORS policy — and the client must explicitly request to include credentials with each request. This mutual agreement ensures that cookie sharing only happens when both parties intend it. Additionally, we must configure cookies themselves with proper security attributes that protect against various attack vectors.
Benefits And Trade-Offs
Understanding when to enable CORS with credentials requires carefully evaluating whether the benefits justify the additional complexity and security considerations for your specific use case.
The Benefits:
Session continuity becomes seamless when properly implemented. Users can navigate between related applications or services without repeatedly authenticating. For example, a user logged into your main application at app.example.com can seamlessly access resources from your API at api.example.com without seeing login prompts.
Centralized authentication enables elegant architectures where a dedicated authentication service manages sessions for multiple applications. This approach simplifies identity management across your entire ecosystem and provides a single source of truth for user authentication state.
Familiar patterns mean your team can leverage existing knowledge of cookie-based authentication rather than learning entirely new paradigms. Most developers understand cookie-based sessions, and many existing systems already use this approach.
Real-World Use Cases:
Microservice architectures often require shared authentication across services running on different domains. For instance, your user service, payment service, and notification service might all need to validate the same user session.
Single sign-on (SSO) systems are perhaps the most common use case. When a user logs into one application in your organization's suite, they expect to be authenticated across all related applications without multiple login prompts.
API gateways frequently need to validate authentication before routing requests to multiple backend services. A central gateway can check session validity once rather than each service implementing its own authentication logic.
Frontend-backend separation has become standard practice, with frontend applications deployed on CDNs for performance while backend APIs run on traditional servers. For example, a React application on Netlify communicating with an ASP.NET Core API on Azure App Service requires cross-origin cookie authentication to maintain user sessions.
The Trade-Offs:
Increased attack surface is the primary concern. Enabling cross-origin cookies creates additional security considerations that must be carefully addressed through proper configuration and ongoing vigilance.
Configuration complexity can lead to subtle vulnerabilities if not done correctly. Precise setup of both CORS policies and cookie settings is essential — a single misconfigured attribute can either break functionality or create security holes.
Browser compatibility varies, as different browsers implement security features differently. While modern browsers generally support the necessary features, you may encounter edge cases that require testing and potentially alternative approaches.
Debugging challenges arise because cross-origin cookie issues can manifest in ways that aren't immediately obvious. Browser developer tools and network inspectors become essential for troubleshooting these scenarios.
Enabling CORS With Credentials
Now let's implement secure cross-origin cookie sharing in our ASP.NET Core application using Minimal APIs. The key is enabling CORS with credentials while specifying exactly which origins can access our resources.
First, let's configure the CORS policy in our application setup:
This configuration does several critical things. The WithOrigins() method specifies exactly which domain can access our resources — we must use a specific origin because wildcard * is incompatible with credentials for security reasons. The AllowCredentials() method is the heart of this configuration, instructing the server to include the Access-Control-Allow-Credentials: true header in responses.
However, this server-side configuration alone isn't sufficient — both sides must opt in for cross-origin cookie sharing to work. While the server uses AllowCredentials(), the client must also explicitly request to include credentials. In browser fetch() calls, this means setting credentials: 'include':
Without credentials: 'include', the browser will not send cookies with the request or store cookies from the response, even if the server has AllowCredentials() configured. This mutual opt-in is by design — it ensures cross-origin cookie sharing only happens when both parties explicitly intend it.
Next, we apply this policy globally and set up route groups for organized endpoint management:
The order matters here — we call UseCors() before mapping endpoints to ensure the CORS middleware processes requests before they reach our handlers. Using MapGroup() with RequireCors() allows us to apply CORS policies to specific route groups, providing fine-grained control over which endpoints accept cross-origin requests with credentials.
Creating Session Cookies
When a user successfully authenticates, we need to create a session cookie that will be sent with subsequent requests. Let's build a realistic login endpoint using Minimal APIs that demonstrates proper cookie creation with all necessary security attributes.
First, let's define our data structures and session storage:
These simple classes represent our login request and session data. We use ConcurrentDictionary rather than a regular Dictionary because ASP.NET Core processes HTTP requests concurrently across multiple threads — simultaneous reads and writes from different requests against a standard Dictionary could cause race conditions or data corruption. ConcurrentDictionary provides built-in thread-safe operations designed for exactly these concurrent access patterns. In a production application, SessionData would likely include additional information like user roles, permissions, and session metadata.
Now let's implement the authentication endpoints using Minimal APIs:
Each cookie attribute serves a specific security purpose. HttpOnly = true prevents client-side JavaScript from accessing the cookie, protecting against XSS attacks. Even if an attacker injects malicious JavaScript into your page, they can't steal the session cookie. The MaxAge setting ensures sessions don't persist indefinitely — expired sessions reduce the window for attack if a session ID is somehow compromised.
The SameSite = SameSiteMode.None attribute is essential for cross-origin scenarios. It explicitly tells the browser that this cookie should be sent with cross-site requests, overriding the default same-site restriction. However, when using SameSite=None, we must also set Secure = true, which ensures the cookie only travels over HTTPS connections, preventing interception. Finally, Path = "/" makes the cookie available across your entire domain rather than just specific paths.
Let's also add logout and session check endpoints:
Validating Session Cookies
Once we've established a session, we need to validate it on protected endpoints. Let's create endpoints that check for valid session cookies and demonstrate the complete request-response flow using Minimal APIs:
The validation process starts by attempting to retrieve the session cookie from the incoming request. ASP.NET Core's HttpRequest.Cookies collection provides convenient access to all cookies sent by the client. If the cookie doesn't exist or is empty, we immediately return an unauthorized response.
If the cookie exists, we validate that it corresponds to an active session in our store. This two-step verification (cookie exists and session is valid) provides defense in depth — even if an attacker somehow obtains a session ID, it must correspond to an active session to grant access.
Testing With HttpClient And CookieContainer:
To test the complete cookie authentication flow programmatically, you can use HttpClient with a CookieContainer. The CookieContainer automatically stores cookies from responses and includes them in subsequent requests, simulating how a browser handles cookies:
Sample Output:
The key insight here is that CookieContainer handles cookie storage and transmission automatically — after the login response sets the sessionId cookie, every subsequent request through the same HttpClient instance includes it. This mirrors browser behavior and is the same pattern you'll use when testing your own cookie-based authentication in the upcoming practice exercises.
Security Best Practices
Implementing cross-origin cookie authentication securely requires adherence to several critical best practices that protect against common vulnerabilities.
-
Origin Whitelisting should be strict and explicit. Never use wildcard
*origins when credentials are enabled — this configuration is not only insecure but also invalid according to CORS specifications. Maintain a carefully curated list of allowed origins, preferably loaded from configuration rather than hardcoded. Consider using environment-specific origin lists so development, staging, and production environments only allow their respective domains. -
Cookie Security Attributes must all be properly configured. The combination of
HttpOnly,Secure, andSameSite=Noneprovides layered defense against different attack vectors. Never compromise on these attributes for convenience — they exist to protect your users. Additionally, implement appropriate cookie expiration strategies. Short-lived session cookies with reasonableMaxAgevalues reduce the window of opportunity for attackers. -
Session Management deserves careful attention. Implement proper session expiration and cleanup to prevent indefinitely valid sessions from accumulating. Consider implementing session renewal for active users while enforcing absolute session timeouts. Store session data securely — use distributed caches like Redis in production rather than in-memory stores, which don't scale and lose data on application restarts.
-
CSRF Protection remains essential even with CORS properly configured. While CORS provides some protection, implementing additional CSRF tokens adds defense in depth. ASP.NET Core provides built-in anti-forgery tokens that should be used for state-changing operations.
-
HTTPS Enforcement is non-negotiable when using
SameSite=None. Modern browsers require theSecureattribute, which means cookies only travel over HTTPS. Configure your hosting environment to redirect all HTTP traffic to HTTPS and use HTTP Strict Transport Security (HSTS) headers to enforce this at the browser level.
Alternative Authentication Methods should be considered for scenarios where cross-origin cookie authentication introduces too much complexity or risk. Token-based authentication using JWT (JSON Web Tokens) provides an alternative that avoids some cookie-related security concerns. Here's how you might implement it with Minimal APIs:
OAuth 2.0 flows offer standardized approaches for many authentication scenarios, particularly when integrating with external identity providers.
Conclusion
In this lesson, we've explored the intricacies of enabling and customizing CORS with credentials for secure cross-origin cookie authentication. You've learned how to properly configure CORS policies that allow credential sharing while maintaining security, how to create session cookies with appropriate security attributes, and how to validate these sessions in protected endpoints.
The key takeaway is that cross-origin cookie authentication requires careful, deliberate configuration at every level — from CORS policies to cookie attributes to session management. While this approach adds complexity compared to same-origin scenarios, it enables powerful architectural patterns like microservices and frontend-backend separation that are fundamental to modern web development.
As you move forward, practice implementing these concepts in your projects while remaining mindful of the security implications. Test your implementation thoroughly, paying special attention to error cases and edge conditions. In the next lesson, we'll continue building on these security foundations to further protect your ASP.NET Core REST API. Keep up the excellent work! 🚀
