Soklet Logo

Core Concepts

CORS

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts certain kinds of XMLHttpRequest and fetch() requests from making cross-domain calls.

This is a common scenario for modern web applications; a website frontend like https://www.soklet.com might make calls to a separate backend system at https://api.soklet.com. The backend approves the frontend's origin and tells the browser "this cross-domain call is OK to make."

Soklet provides high-level convenience CORS constructs for standard workflows. To support specialized workflows, lower-level typesafe functionality for reading CORS request data and writing CORS responses is also available.


Standard Workflow

Most applications fall into one or more of these categories:

No custom code is required - just pick the one that's appropriate, wire it in, and you're done.

If you need fine-grained control over authorization logic or response writing, see the Custom Workflow documentation.

Authorize No Origins

Soklet is configured to use CorsAuthorizer::withRejectAllPolicy by default, which declines to authorize CORS requests regardless of the value specified in the Origin request header.

SokletConfig config = SokletConfig.withServer(...)
  // Example for illustration - Soklet already uses this CorsAuthorizer
  // by default; you don't need to specify explicitly like this
  .corsAuthorizer(CorsAuthorizer.withRejectAllPolicy())
  .build();

References:

Authorize Whitelisted Origins

This is usually what you want in a production system - a whitelisted set of origins from which to allow CORS requests.

Set<String> allowedOrigins = Set.of("https://www.revetware.com");

SokletConfig config = SokletConfig.withServer(...)
  .corsAuthorizer(CorsAuthorizer.withWhitelistedOrigins(allowedOrigins))
  .build();

Alternatively, if your origin set is dynamic, a Function<String, Boolean> variant is available:

Function<String, Boolean> whitelistAuthorizer = (origin) -> {
  return origin.equals("https://www.revetware.com");
};

SokletConfig config = SokletConfig.withServer(...)
  .corsAuthorizer(CorsAuthorizer.withWhitelistAuthorizer(whitelistAuthorizer))
  .build();

To reduce CSRF attack surface area, the above CorsAuthorizer instances will omit the Access-Control-Allow-Credentials response header.

However, you might prefer to allow credentials if, for example, if your system uses cookie-based authentication and has appropriate CSRF defenses in place.

You may specify an allowCredentialsResolver function like this:

Set<String> allowedOrigins = Set.of("https://www.revetware.com");

// Custom "Access-Control-Allow-Credentials=true" decider
Function<String, Boolean> allowCredentialsResolver = (origin) -> {
  return true;
};

SokletConfig config = SokletConfig.withServer(...)
  .corsAuthorizer(CorsAuthorizer.withWhitelistedOrigins(
    allowedOrigins, allowCredentialsResolver
  ))
  .build();

...or like this for the whitelistAuthorizer variant:

Function<String, Boolean> whitelistAuthorizer = (origin) -> {
  return origin.equals("https://www.revetware.com");
};

// Custom "Access-Control-Allow-Credentials=true" decider
Function<String, Boolean> allowCredentialsResolver = (origin) -> {
  return true;
};

SokletConfig config = SokletConfig.withServer(...)
  .corsAuthorizer(CorsAuthorizer.withWhitelistAuthorizer(
    whitelistAuthorizer, allowCredentialsResolver
  ))
  .build();

These "whitelist only" CorsAuthorizer instances will do the following for CORS preflight requests:

References:

Authorize All Origins

Use CorsAuthorizer::withAcceptAllPolicy to permit all CORS requests regardless of the value specified in the Origin request header. Can be useful for local development and experimentation.

SokletConfig config = SokletConfig.withServer(server)
  // Don't use this in production!
  .corsAuthorizer(CorsAuthorizer.withAcceptAllPolicy())
  .build();

Be Careful!

This is unsafe; please don't use it in a production system. See Authorize Whitelisted Origins for a production-ready solution.

This accept-all CorsAuthorizer will do the following for CORS preflight requests:

References:

Custom Workflow

A CORS request is defined as a request with an Origin header (in practice, this is the protocol + host + optional port in your browser's URL bar, e.g. https://www.soklet.com). Browsers will automatically set the Origin for cross-domain requests. It is the responsibility of the server to send a response with appropriate headers to let the browser know if the request is acceptable.

If a request is not a simple request, the browser will automatically create and send a special preflight request beforehand - an OPTIONS request which includes headers that say "this is the operation I want to perform, is it OK?" It is the server's responsibility to send response headers to accept or reject the preflight request. If the preflight is accepted, the browser will then send the real request.

Soklet provides two customization hooks:

Authorizing CORS Requests

The CorsAuthorizer interface has two methods: one which authorizes a CORS preflight, and one which authorizes a CORS non-preflight. For both methods, you may return Optional<T>::empty to reject the CORS request. To accept the CORS request and specify the CORS response headers to write downstream, return CorsPreflightResponse or CorsResponse, respectively. To control how the headers are ultimately written, see Writing CORS Responses.

SokletConfig config = SokletConfig.withServer(server)
  .corsAuthorizer(new CorsAuthorizer() {
    @Nonnull
    @Override
    public Optional<CorsPreflightResponse> authorizePreflight(
      @Nonnull Request request,
      @Nonnull CorsPreflight corsPreflight,
      @Nonnull Map<HttpMethod, ResourceMethod> availableResourceMethodsByHttpMethod
    ) {
      // Your authorization logic goes here for preflight CORS requests.
      // Return a CorsPreflightResponse to set the appropriate CORS response headers.
      // Return an empty value to skip those headers (reject the request).
      return Optional.empty();
    }

    @Nonnull
    @Override
    public Optional<CorsResponse> authorize(
      @Nonnull Request request,
      @Nonnull Cors cors
    ) {
      // Your authorization logic goes here for non-preflight CORS requests.
      // Return a CorsResponse to set the appropriate CORS response headers.
      // Return an empty value to skip those headers (reject the request).
      return Optional.empty();
    }
  })
  .build();

For example, here is how you might implement a CorsAuthorizer which greenlights any soklet.com subdomain and specifies custom CORS response headers:

SokletConfig config = SokletConfig.withServer(server)
  .corsAuthorizer(new CorsAuthorizer() {
    // Any subdomain under soklet.com is permitted
    boolean isOriginWhitelisted(@Nonnull String origin) {
      return origin.matches("^https://([a-zA-Z0-9-]+\\.)+soklet\\.com$");
    }

    @Nonnull
    @Override
    public Optional<CorsPreflightResponse> authorizePreflight(
      @Nonnull Request request,
      @Nonnull CorsPreflight corsPreflight,
      @Nonnull Map<HttpMethod, ResourceMethod> availableResourceMethodsByHttpMethod
    ) {
      // Only greenlight our soklet.com subdomains
      if (isOriginWhitelisted(corsPreflight.getOrigin()))
        return Optional.of(
          CorsPreflightResponse.withAccessControlAllowOrigin(corsPreflight.getOrigin())
            .accessControlAllowMethods(availableResourceMethodsByHttpMethod.keySet())
            .accessControlAllowHeaders(Set.of("X-Example-Header"))
            .accessControlAllowCredentials(true)
            .accessControlMaxAge(Duration.ofMinutes(5))        
            .build()
        );

      return Optional.empty();
    }    

    @Nonnull
    @Override
    public Optional<CorsResponse> authorize(
      @Nonnull Request request,
      @Nonnull Cors cors
    ) {
      // Only greenlight our soklet.com subdomains
      if (isOriginWhitelisted(cors.getOrigin()))
        return Optional.of(
          CorsResponse.withAccessControlAllowOrigin(cors.getOrigin())
            .accessControlExposeHeaders(Set.of("X-Example-Header"))
          .build()
        );

      return Optional.empty();
    }
  })
  .build();

References:

Writing CORS Responses

Experts Only

Unless you have special requirements, you should prefer to use Soklet's default ResponseMarshaler CORS response writing instead of rolling your own.

The output of your CorsAuthorizer is fed into your ResponseMarshaler so it can handle the details of writing HTTP headers/status/etc. to the response.

For example, suppose you'd like to customize how Soklet writes CORS preflight request rejections. Just provide a CorsPreflightRejectedHandler to your ResponseMarshaler:

// Write a custom "CORS preflight rejected" response
CorsPreflightRejectedHandler corsPreflightRejectedHandler = (
  @Nonnull Request request,
  @Nonnull CorsPreflight corsPreflight
) -> {
  Integer statusCode = 403;
  Charset charset = StandardCharsets.UTF_8;
  String body = "Sorry, CORS preflight rejected.";

  // Returns a 403 with a custom response body
  return MarshaledResponse.withStatusCode(statusCode)
    .headers(Map.of("Content-Type", Set.of(
      format("text/plain; charset=%s", charset))
    ))
    .body(body.getBytes(charset))
    .build();
};

// Supply our custom handler to the marshaler
SokletConfig config = SokletConfig.withServer(...)
  .responseMarshaler(
    ResponseMarshaler.withDefaults()
      .resourceMethodHandler(...)
      .corsPreflightRejectedHandler(corsPreflightRejectedHandler)
      .build()
  )
  .build();

Hooks are also provided for writing "allowed" CORS responses:

// Write a custom "CORS preflight allowed" response
CorsPreflightAllowedHandler corsPreflightAllowedHandler = (
  @Nonnull Request request,
  @Nonnull CorsPreflight corsPreflight,
  @Nonnull CorsPreflightResponse corsPreflightResponse
) -> {
  // Return a MarshaledResponse with headers like
  // Access-Control-Allow-Origin, Vary, etc.
};

// Supplement responses for requests that have "passed" preflight
CorsAllowedHandler corsAllowedHandler = (
  @Nonnull Request request,
  @Nonnull Cors cors,
  @Nonnull CorsResponse corsResponse,
  @Nonnull MarshaledResponse marshaledResponse
) -> {
  // Supplement the provided MarshaledResponse with
  // Access-Control-Allow-Origin, Vary, etc. and return it
};

// Supply our other custom handlers to the marshaler
SokletConfig config = SokletConfig.withServer(
  Server.withPort(8080).build()
).responseMarshaler(
  ResponseMarshaler.withDefaults()
    .resourceMethodHandler(...)
    .corsPreflightAllowedHandler(corsPreflightAllowedHandler)
    .corsAllowedHandler(corsAllowedHandler)
    .build()
).build();

You might also choose to ignore Soklet's default ResponseMarshaler implementation and roll your own instead, which would look like this:

SokletConfig config = SokletConfig.withServer(
  Server.withPort(8080).build()
).responseMarshaler(new ResponseMarshaler() {
  @Nonnull
  @Override
  public MarshaledResponse forCorsPreflightRejected(
    @Nonnull Request request,
    @Nonnull CorsPreflight corsPreflight
  ) {
    // TODO: your custom implementation
  }

  @Nonnull
  @Override
  public MarshaledResponse forCorsPreflightAllowed(
    @Nonnull Request request,
    @Nonnull CorsPreflight corsPreflight,
    @Nonnull CorsPreflightResponse corsPreflightResponse
  ) {
    // TODO: your custom implementation
  }

  @Nonnull
  @Override
  public MarshaledResponse forCorsAllowed(
    @Nonnull Request request,
    @Nonnull Cors cors,
    @Nonnull CorsResponse corsResponse,
    @Nonnull MarshaledResponse marshaledResponse
  ) {  
    // TODO: your custom implementation
  }

  // Other ResponseMarshaler methods elided
}

References:

Previous
Server Configuration