In today’s IT world, growth and competition lead industry to adopt faster releases of their software products and services to market. To avoid the cost of building services, enterprises subscribe to other readily available services. Businesses emphasize safeguarding their business data while consuming services from providers, and such service providers facilitate robust and secure communication channels to their consumers. Often enterprise business operations are fed by software applications and are secured with intranet applications contexts, where hostnames are known with domain names. There is no need for Cross-Site communication. With this new shift, services are securely exposed to consumers, but features are also consumed from different organizations over HTTP.
As organizations are widening their software applications to consume readily available services over HTTP, the security approaches are different than what is used for intranet applications for enterprise application development. Today, you’ll hear terms like Cross-Site or CORS (Cross-Origin resource sharing). CORS means that two independent applications communicate as publisher and subscriber, to secure the resources or content or data during communication (over HTTP request or response). There are different approaches, configurations, and best practices. Here authentication and authorization play a vital role to safeguard the participating applications. This article focuses on how authentication and authorization need special consideration when applications have multiple origins. It also looks at how a cookie-based authentication implementation works for a cross-origin site or under CORS.
Use Case: Authentication and Authorization
Organization “Company-E” has a homegrown Enterprise Health Record (EHR) E1, and organization “Company-P” has the Patient Portal P1 application to allow patients to view their own health records and communicate with their providers. Company-E and Company-P come to an agreement that allows P1 to subscribe to the service of E1 to facilitate the health data to the patient or patient’s responsible party. The hosting of the applications will remain the same, i.e., E1 is hosted and maintained by Company-E and P1 is hosted and maintained by Company-P. To enable this engagement, both E1 and P1 application need some enhancements to enable cross-origin communication over HTTP. Below is the simple requirement from each application
E1: expose secured service and allow only P1 to consume. Data send/received can happen on this service with users who are authenticated and have been privileged to do such activities.
P1: ability to provide user credentials to E1 services to send and receive data
The current E1 and P1 applications have been described, and the new requirements for enabling secure communication between them are documented. The next step is to diagram what needs to be accomplished. Figure 1 is a diagram depicting the logical view of P1-WebUI and E1-RESTful API service as per the requirements.
Figure 1: Logical view of P1 and E1 requirements
Figure 1 can be described as follows:
- P1 is browser rendering web user interface created using React library, hosted by Company-P with URI https://healthui
- E1 has RESTful API created with .NET core WebAPI, hosted by Company-E with URI http://healthapi
- For authentication,
- P1 sends an HTTP request with the body encapsulating valid user credentials
- E1 validates credentials and, once successful, sends an HTTP (HTTP-only) cookie as a response to P1
- P1’s HTTP client, i.e. browser, includes Auth.Cookie to each subsequent HTTP request
- For authorization,
- E1 validates each HTTP request other than sign-in, if the user does not have privileges, then response with status code 403 (Forbidden)
Begin implementing the solution by following the captured approach notes. Note that the entire solution is not covered; only key points for implementing a solution are included. You can find the entire solution here.
The E1 API is RESTful created with .NET core 3.1 Web-API. First, you need to configure the necessary settings to enable (cross-site) CORS HTTP request processing. The http://healthui app should be able to read or write data based on its functional business operations. To begin, open the startup.cs file of a Web-API project. Under the
ConfigureService method, add a CORS service to reference
IServiceCollection. Follow Figure 2, which enables CORS middleware to handles cross-origin requests.
Our cookie-based-authentication and authorization settings are fulfilled by the three core namespaces spaces of AspNetCore and they are listed as below in Figure 2.
Figure 2: Using statements
Line#1 from the above snippet facilitates all settings for enabling authentication and Line#2 facilitates enabling http cookies for authentication. Line#3 facilitates to establish authorization for each incoming request and as per needs of application authorization rules can be built.
NOTE that the entire application is not covered in this article. It covers only the key aspects of CORS cookie-based authentication. Figure 3 contains code to enable the CORS middleware.
Figure 3: Enable CORS middleware
- Line#29, AddCors extension method is setup method, Action<CorsOption> is parameter to configure CORS with provided option
- Line#31, a call to method options.AddPolicy, XAuthOrigins read-only string variable passed as the first parameter to specify the name of the policy and the second parameter is CorsPolicyBuilder
- Line#34, adds hostname to the CorsPolicy.Orgins collection and specifies P1’s hosted application URI, http://healthui. This URI can be on port 80 HTTP or 443 HTTPS
- Moving ahead with the same policy builder at Line#35, AllowAnyMethod method allows any HTTP request method, i.e. GET, HEAD, or POST, etc.
- Line#36, AllowAnyHeader allows request headers such as “content-type” or “content-language”.
- Line#37, AllowCredentials method is to allow cross-origin credentials. The HTTP response includes a header “Access-Control-Allow-Credentials” which tells the browser that the server allows credentials for cross-origin-requests. This is how it works:
- If the E1 API sent credentials to the calling application P1’s browser and the response header doesn’t include “Access-Control-Allow-Credentials”, then the browser doesn’t expose response to the P1 WebUI, and the request fails
- The CORS specification states that all origin ‘*’ is invalid if “Access-Control-Allow-Credentials” header is present. In this case, at Line#34, there is only the known hostname “http://heathui.”
Once the CORS middleware policy with name is built with the required configuration, the same policy name shall be used to add the CORS middleware to P1’s RESTful API to allow cross-domain HTTP requests as shown in Figure 4.
Figure 4: Add the CORS middleware to allow cross-domain HTTP requests
As highlighted at Line#68, it’s necessary to add CORS middleware for P1 API’s application builder at the first line in the configuration method.
At this stage, CORS middleware is added with the desired configuration. The next step is to enable authentication and authorization middleware to establish secured cross-site HTTP communication. Inside the same
Configuration method. Line#72 is adding authentication middleware, and Line#75 is adding authorization middleware to P1’s RESTful API application builder.
Next, go back to
ConfigurationServices method, add authentication and authorization services with required configuration policy options. Figure 5 demonstrates adding services to the service collection.
Figure 5: Adding services
The important points to note are as follows:
- Line#41, call to the method AddAuthorization adds the authorization policy to the service collection parameter services. Its authorization parameter sets the default policy with new the AuthorizationPolicyBuilder instance having “Cookies” authentication scheme. Specifies user must be authenticated with method “RequireAuthenticatedUser” to access any resource
- Line#46, call to the method AddAuthentication adds authentication options to service collection parameter services, “DefaultAuthenticateScheme” and “DefaultChallengeScheme” are set to “Cookies”, which is a constant value of “CookieAuthenticationDefaults.AuthenticationScheme” which enables cookie-based authentication. Each request must carry the valid cookie to access the API resource
- Line#51, call to the method AddCookie adds the cookie authentication options to the authentication configuration
- Line#53, the same site property Cookie.SameSite is set to SameSiteMode.None, i.e. to allow cross-site cookie use
- Line#54, cookie is always set to secure and, all calls to API needs to be done with HTTPS
- Line#55, Cookie.HttpOnly set to true. The cookie will only be passed to HTTP requests and is not made available to script on the page
- Line#56, UserAuthenticationEvent type is used to get events instance for authenticating incoming cookie from an HTTP request
- Rest of the lines from, #57 to #60, sets login, logout, access denied paths and sliding expiration to true which will issue new expiration time to the valid and authenticated HTTP request
- Line#62, adds the UserAuthenticationEvent type to the service collection instance
The code for setting and enabling the CORS plus cookie-based secured authorization and authentication validator for validating incoming requests is complete. Take a look in brief what “UserAuthenticationEvent” has to intercept each incoming request, and how it verify cookie and act accordingly to with HTTP response as shown in Figure 6.
Figure 6: Verify cooking and response
UserAuthenticationEvent overrides the ValidatePrincipal method of the base class CookieAuthenticationEvents, fetch user’s email from principal claims collection and check the validity of it. If validation fails then:
- Reject the incoming principal from cookie
- Set the HTTP status code to 401 Unauthorized
- Sign out current HTTP context with “Cookie” authentication scheme
Secure API Resource
Only the “sign-in” route should allow anonymous access. There is no need to have authentication and authorization checks; hence the
AllowAnonymous attribute is added to the
Authenticate method, i.e. Line#20 in
UserAccountController shown in Figure 7.
Figure 7: Allow anonymous sign in
All other controllers except
UserAccountController should have
AuthoriseUser attribute, to validate principal from incoming HTTP request context show in Figure 8.
Figure 8: The AutorizeUser attribute
The code shown below in Figure 9 is the implementation of an authorization check. Here you can add your own rules to restrict the user if they do not have rights to access resources for which the HTTP request has been made.
Figure 9: The AuthorizeUserAttribute class
- Line#13, checks the user who made HTTP request. Its HTTPContext has user identity authenticated or not. If the request is not authenticated then, assign
AuthorizationFilterContextinstance, which will add HTTP status code 401 (Unauthorized) to HTTP response.
- If the user is authenticated but not found in the system, then assign
AuthorizationFilterContextinstance, which will add HTTP status code 403 (Forbidden) to HTTP response.
P1’s Web UI is built with React web UI library, a browser rendering single page application, to have a component-based UI. The Axios react npm library (promise-based HTTP client) is used for making HTTP requests to E1’s RESTful API. Enable consumer P1 to make calls to cross-site calls under the secured policy of E1.
Remember E1’s API has been enabled to allow credentials so the browser can consider credentials in subsequent HTTP requests. Yet to inform browsers that do include credentials, ajax HTTP request call needs to include attribute
withCredential: true. In Figure 10 below, Line#15 creates and enables the HTTP-Client
apiClient instance of
axios to include credentials.
Figure 10: Enable HTTP client
P1’s web UI uses
this.apiClient for making an HTTP request to https://healthapi. Due to the CORS setting at E1 API, the client-side script will not be able to access the authentication cookie. Still, the browser does recognize and manage inclusion of the authentication cookie in subsequent HTTP requests. That’s the only configuration you need to take care of at client-side. Now to look at HTTP request and HTTP response attributes to know what is happening under cross-site communication.
All working together
With the help of the developer toolbar of the Chrome browser, you will test, verify and see inside of outgoing requests and incoming responses over the network. The Figure below is the snapshot of a network profile captured during the sign-in attempt. The URI is http://healthui/account/useraccount/signin. Many headers can be found under request headers. The request origin is http://healthui; authority header is host, i.e. healthapi; the path is /account/useraccount/signin; the method is POST on HTTPS scheme; header sec-fetch-site value is cross-site. This indicates the request has been made to cross-site. Figure 11 shows the Request Headers.
Figure 11: Request Headers
Review the response headers one by one from this HTTP request:
- access-control-allow-origin: http://healthui, this indicates whether the response can be shared with the requesting application from the given origin(host)
- set-cookie: xxxxxxx…. authentication cookie sent by https://healthapi with the response object
Figure 12 shows the Response Headers
Figure 12: Response Headers
After looking at the response header and sending the authentication cookie, review look at the attributes of the response cookie shown in Figure 13.
Figure 13: The Response Cookies
The name of the cookie is .AspNetCore.Cookies; value is long alphanumeric string; issuing domain is healthapi; HttpOnly and Secured are enabled, and SmeSite is none. These are all configuration settings found in startup.cs in E1’s RESTful API.
Once P1 WebUI is authenticated, the browser will add an authentication cookie to all subsequent HTTP requests. You can see this from the outgoing request’s headers captured by network profile. A cookie named .AspNetCare.Cookies is an authentication cookie which is included by the browser, and the server will validate to process the response. See Figure 14.
Figure 14: The authentication cookie
If any other application tries to access E1’s API, then it will fail with this server error:
Access to XMLHttpRequest at ‘https://helathapi/account/UserAccount/signin’ from origin ‘http://DoubleCare’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
E1’s API is secured and available to P1’s WebUI to get data. If any other app tries to access the RESTful API, then it won’t gain access to it, and the server will throw CORS origin or cross-site exception to the caller.
Today, there are many consumable services available for business applications. Instead of inventing services, industry prefers to sign-up for services. Such solution works best for healthcare, e-commerce, financial, tours & travels, hospitality and others.
This example provides the solution and meets what is needed:
- Applications or distributed and owned by individual organization, one is a subscriber and other is a distributor
- Secured authentication to enable safe data communication over HTTP
- Cross-Site authentication/authorization, let the browser handle authenticated cookies for HTTP requests
- Distributor validates incoming cookie before handling it to action for accessing resource(s)
The source code for this project is available here https://github.com/hi10p/CookieCORS.git