Skip to content

Instantly share code, notes, and snippets.

@tjquinno
Last active May 1, 2020 14:49
Show Gist options
  • Save tjquinno/d7336ad17268116fc6b7ed2c238f0cc5 to your computer and use it in GitHub Desktop.
Save tjquinno/d7336ad17268116fc6b7ed2c238f0cc5 to your computer and use it in GitHub Desktop.

CORS in Helidon SE

How does it work?

Recall that your Helidon SE application constructs rules for Helidon to use in routing HTTP methods and paths to your application endpoints. You use the same approach to add CORS support to the HTTP methods and paths in your application that need it.

To use the Helidon CORS support, your application:

  1. creates one or more instances of CorsSupport, each set up according to how you want your application endpoints to work with CORS, and
  2. uses the CorsSupport instances in preparing the routing rules.

In this way, you associate specific CORS behavior with each endpoint/HTTP method combination in your application. You prescribe this CORS behavior in one or more Helidon CrossOriginConfig objects.

CrossOriginConfig - the basic unit of CORS behavior

Your application can create a CrossOriginConfig object using a Config object containing one or more of these settings:

Helidon config keys Default CORS header name
allow-credentials false Access-Control-Allow-Credentials
allow-headers ["*"] Access-Control-Allow-Headers
allow-methods ["*"] Access-Control-Allow-Methods
allow-origins ["*"] Access-Control-Allow-Origins
expose-headers none Access-Control-Expose-Headers
max-age 3600 Access-Control-Max-Age
enabled true n/a

All of these except enabled correspond to the headers used in the CORS protocol. If the configuration sets enabled to false, then the Helidon CORS implementation ignores that cross-origin configuration entry.

Common use cases

Both the Helidon routing mechanism and the Helidon CORS support are very flexible. You can combine them in many ways to add CORS support to your endpoints. Here we describe a few of the likely use cases and how to implement them. These approaches use a combination of code in your application and configuration.

Unrestricted CORS access ("the wild west")

Some applications might not be susceptible to the dangers of unmanaged cross-origin resource sharing. For example, perhaps your application supports only GET HTTP requests and the responses never contain sensitive information. You might want your endpoints to honor the CORS protocol but without restricting resource sharing.

Use a single CrossOriginConfig instance with default settings and apply that to all incoming traffic. Using the GreetService from the Helidon SE quickstart example, the following code creates a Routing instance you can use to start your Helidon SE server with all endpoints providing unrestricted CORS support:

Routing routing = Routing.builder()
                     .register(JsonSupport.create())
                     .register(CorsSupport.create())
                     .register("/greet", new GreetService())
                     .build();

Using categories of CORS access

In this approach:

  1. Choose what different categories of CORS support you want to support in your application. For example, you might want to allow unrestricted sharing in certain cases and stricter access in others.
  2. Prepare a configuration source that contains the CORS set-up for each of the categories.
  3. Add code to your application to:
    1. read the configuration, creating a CrossOriginConfig object for each category, then
    2. apply the correct CrossOriginConfig instance to each HTTP method/path combination.

Helidon configuration lets you use multiple configuration sources. These examples use the default application.yaml config file to hold the CORS configuration for the different categories of access but you are not limited to that; any config source your application uses will work.

Choose CORS categories

The purpose of CORS is to let you carefully relax the restrictions imposed by the same-origin policy https://en.wikipedia.org/wiki/Same-origin_policy. For example, suppose you want to allow unrestricted access for GET, HEAD, and POST requests (what CORS refers to as "simple" requests), but permit other types of requests only from the two origins foo.com and bar.com. This means you have two categories of CORS configuration: relaxed for the simple requests and stricter for others. In practice, you can use as many categories as makes sense for your application.

Prepare the configuration

Create or update your application's default configuration file:

application.yaml

restrictive-cors:
  allow-origins: ["foo.com", "bar.com"]
  allow-methods: ["PUT", "DELETE"]
  
open-cors: # defaults to "*" for all the "allows" values
  max-age: -1

You can choose any key values (restrictive-cors, open-cors in this example) as long as your application code uses the same keys to retrieve each set of configuration.

Load the CORS configuration

import io.helidon.config.Config;
...
Config appConfig = Config.create(); // loads config from the default sources, including application.yaml
            
CrossOriginConfig restrictiveConfig = CrossOriginConfig.create(appConfig.get("restrictive-cors"));
CrossOriginConfig openConfig = CrossOriginConfig.create(appConfig.get("open-cors"));

Build the routing

The following code sets up restrictive CORS access for PUT and DELETE requests and

CorsSupport restrictiveCors = CorsSupport.create(restrictiveConfig);

Routing routing = Routing.builder()
                     .register(JsonSupport.create())
--> wrong; need to fix                 .any(CorsSupport.builder().addCrossOriginConfig(openConfig).build()
                     .put(restrictiveCors)
                     .delete(restrictiveCors)
                     .register("/greet", new GreetService())
                     .build();

A few notes:

  1. We created the open CorsSupport instance using the config convenience method; we did not need a local variable to hold it because we use it only once.
  2. We reuse the restrictiveCors CrossOriginConfig instance in setting up the routing for PUT and DELETE requests.
  3. We have not changed any of the GreetService code itself that implements the business logic of the application.
  4. We invoke any, put, and delete with the CORS set-up before invoking register for the GreetService. This makes sure that the Helidon CORS logic runs first -- particularly for PUT and DELETE requests -- so unwanted requests are prevented from reaching the application's endpoints.
  5. This code applies CORS based only on the HTTP method, not on the endpoints' paths.
  6. Note that the greeting application does not implement a DELETE endpoint. We set up restrictive CORS enforcement for DELETE requests anyway in this example because:
  7. In general, if you are protecting puts you would probably also want to protect deletes and we wanted to illustrate that.
  8. Future enhancements to your greeting service might add and endpoint for DELETE and this way the routing code is already in place to apply resctrictive CORS access to DELETE requests.

Overriding or changing the CORS configuration

Because the application reads the CORS set-up for the categories from configuration, you or others who deploy your application can modify that CORS set-up by revising or overriding the configuration, without needing any changes to the application code. By default, Helidon configuration allows multiple sources for the default application config (such as system properties in addition to the application.yaml file). Different deployments of your application can use different CORS rules without editing applicaiton.yaml or repackaging the application.

By writing your application to load CORS information from configuration, you give yourself and deployers of your application all the flexibility of the Helidon config system to assign and override the CORS settings.

Applying CORS by endpoint path

Suppose your application includes some administrative features that should permit only very restricted resource sharing. Add a new section to your application config file and modify your application to read the CORS set-up for the admin functions from the config and set up routing using the CORS constraints.

application.yaml

...
admin-cors:
  allow-origins: ["mycompany.com"]
...

Assume you have added a new Service implementation to your application called GreetAdminService. To the earlier example code add this:

CorsSupport adminCors = CorsSupport.mappedCreate(appConfig.get("admin-cors"));

Routing routing = Routing.builder()
                     .register(JsonSupport.create())
                     .any(CorsSupport.builder().addCrossOriginConfig(openConfig).build())
                     .put(restrictiveCors)
                     .delete(restrictiveCors)
                     .register("/greet", new GreetService())
                     .register("/greet/admin", adminCors, new GreetAdminService()) // <-- this is new
                     .build();

All admin operations will have the /greet/admin prefix and enforce the resource sharing constraints your application loaded from the configuration.

Full override from configuration

You can allow deployers of your application to override completely the CORS configuration in your application, based on path matching and HTTP method.

Revise how the application builds the routing:

MappedCrossOriginConfig mapped = MappedCrossOriginConfig.create(appConfig.get("cors"));

Routing routing = Routing.builder()
                     .register(JsonSupport.create())
                     .any(CorsSupport.builder().addCrossOriginConfig(openConfig).build())
                     .put(restrictiveCors)
                     .delete(restrictiveCors)
                     .register("/greet", new GreetService())
                     .register(mapped)  // <--- this is new
                     .build();

Add overrides to the configuration

The mapped cross-origin config can take path mapping information as well as the CORS set-up itself. The configuration can have a new section containing overrides:

application.yaml

...
cors:
  - path-prefix: /greet
    allow-origins: ["foo.com"]
    allow-methods: ["PUT", "DELETE"]
  - path-prefix: /other
    allow-origins: ["bar.com"]

By adding just two lines to your application you allow deployers of your application to override any CORS setting for any endpoint. The overriding configuration specifies endpoint path expressions which Helidon CORS uses to match actual endpoint paths in the application.

We do not recommend using this approach as the primary way to assign CORS behavior to endpoints. If you decide to change the path or subpaths for the endpoints you need to change the Java code and the configuration. Instead, rely on mapped CORS configuration for overriding only and primarily use the CORS categories approach above to prepare the normal CORS set-up for your application.

The rest is probably not needed

Original table:

Helidon config keys CrossOriginConfig.Builder method Default CORS header name
allow-credentials allowCredentials false Access-Control-Allow-Credentials
allow-headers allowHeaders "*" Access-Control-Allow-Headers
allow-methods allowMethods "*" Access-Control-Allow-Methods
allow-origins allowOrigins "*" Access-Control-Allow-Origins
expose-headers exposeHeaders none Access-Control-Expose-Headers
max-age maxAgeSeconds 3600 Access-Control-Max-Age
enabled enabled true n/a

Creating CrossOriginConfig objects

Invoke attribute-level methods

CrossOriginConfig crossOriginConfig = CrossOriginConfig.builder() 
                                        .allowCredentials(true)
                                        .allowHeaders(hdr1, hdr2, ...)
                                        .allowMethods(mth1, mth2, ...)
                                        .allowOrigins(or1, or2, ...)
                                        .exposeHeaders(hdrA, hdrB, ...)
                                        .maxAgeSeconds(age)
                                        .enabled(true)
                                        .build();

You can omit any or all of the attribute-setting method invocations, in which case defaults or previous settings on the same CrossOriginConfig.Builder object prevail.

Load from configuration

Use the CrossOriginConfig.Builder config method to partly or completely set up CORS information from a Helidon Config object. You can mix calls to the config method and the attribute-setting methods; the last one invoked overrides any prior settings.

To load just from configuration, use the CrossOriginConfig.create(Config) method (which simply functions as CrossOriginConfig.builder().config(myConfig).build()).

Combining config and attribute-level assignments

You can create a CrossOriginConfig instance using configuration and the attribute-level methods together. Think of the CrossOriginConfig.Builder.config method as a sequence of attribute-level method invocations using the contents of the config object. You can invoke the builder's config method multiple times, and you can invoke config and the attribute-level methods in any order. Remember that the last assignment to a setting wins.

Updating Helidon routing for CORS

Your application needs to link each CrossOriginConfig object to the endpoint/HTTP method combinations it should affect. You create routing rules for CORS very similarly to how you do with your application endpoints.

Helidon SE routing review

Recall that the Helidon SE routing rules approach lets you define rules in six ways. For each request, Helidon SE routing uses the first entry in this table that matches. The "Case" column refers to the CorsSupport cases below.

HTTP method-specific Path-specific Routing.Builder Java method Case Notes
X X get(pathExpr, handler...) 1 Same for put, post, etc. Routing.Builder methods. Associates the rule with one HTTP method and one path expression.
_ X any(pathExpr, handler...) 1 Applies to any HTTP method.
X _ get(handler...) 2 Applies to any HTTP GET request.
_ X register(pathExpr, service...) 3 Applies to any request matching the path expression.
_ _ any(handler...) 2 Applies to all requests.
_ _ register(service...) 4 Applies to all requests.

CorsSupport as the unit of CORS-related routing

Your application passes a CorsSupport object as the handler or service to the methods above. You associate one or more CrossOriginConfig objects with each CorsSupport object you create. Exactly how you construct the CorsSupport instance depends on the case, detailed next. Note that you can (but in some cases will not) give a path-matching expression when you link the CrossOriginConfig object with the CorsSupport object.

Case 1 - handler path specified to Routing.Builder method

The CorsSupport object wraps a single CrossOriginConfig' object. You associate it with a path expression as a parameter to the Routing.Buildermethod along with theHandler` that implements that operation.

CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin(restrictive).build();
Routing.Builder builder = Routing.builder()
                            .get("/greet", corsSupport); // or put, post, options, any, etc.

Need to add app handlers to these examples

Case 2 - handler path specified to CorsSupport.Builder

The CorsSupport object holds one ore more CrossOriginConfig instances, each potentially mapped to different path expressions.

CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin("/greet", restrictive).build();
Routing.Builder builder = Routing.builder()
                            .get(corsSupport); // or put, post, options, any, etc.

Case 3 - service path specified to Routing.Builder

The CorsSupport object wraps one CrossOriginConfig object. You associate it with a path expression as a parameter to the Router.Builder.register method.

CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin(restrictive).build();
Routing.Builder builder = Routing.builder()
                            .register("/greet", corsSupport);

Case 4 - service with path specified to CorsSupport.Builder

The CorsSupport object holds one ore more CrossOriginConfig instances, each potentially mapped to different path expressions.

CorsSupport corsSupport = CorsSupport.builder().addCrossOrigin("/greet", restrictive).build();
Routing.Builder builder = Routing.builder()
                            .register(corsSupport);

Which is the right way?

It depends. If you can, avoid case

Think of each CorsSupport object as a collection of one or more CrossOriginConfig objects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment