Broadleaf Microservices
  • v1.0.0-latest-prod

Security

Spring Security

Broadleaf leverages Spring Security and Spring Security OAuth as the default vehicle for securing the microservice architecture. Each microservice leverages OAuth2 by default and requires a valid bearer token in the header for authorization. The admin client application interacts with Broadleaf’s Auth service and that relationship is stateless. Logging into, and interacting with, the auth service is strongly protected against CSRF. Furthermore, OAuth token acquisition (via code grant flow) follows OAuth2 best practices. Among other things, this includes secure utilization of the state parameter for oauth requests, and full redirect url whitelisting. All interactions are via TLS (HTTPS).

Variations on this theme include leveraging alternate SSO strategies in tandem with Broadleaf’s Auth service for admin tool interaction. Also, storefront users may leverage Broadleaf’s Auth service for authentication, or another identity provider (IDP) may be leveraged. For the latter, it is most common to leverage an OpenID Connect (OIDC) style flow and pass the resulting token to Broadleaf’s resource tier API as a header value. In this case, customization to the Spring Security token validation phase is required to validate according to the identity provider’s instructions. A third-party IDP may also drive additional customization for more advanced customer service (CSR) features.

Broadleaf also leverages all default Spring default security headers and security features, and more may be enabled as needed.

Other Factors

This document does not represent an exhaustive list of secure coding practices, but here are several notables:

  • Logging - Broadleaf does not log Personal Identifiable Information (PII) to a log file. Sensitive payment information is also not logged to a log file.

  • Injection - Broadleaf continuously reviews all code for injection possibilities. This spans a range of subjects, including: logs, JPA/SQL, SPEL, etc…​

  • CSRF and XSS - Broadleaf secures the system against CSRF attacks - especially as they relate to OAuth interaction and Auth service interaction. Broadleaf employs XSSRequestProcessingService, which is leveraged via an HTTP filter to validate user supplied data against a set of content rules. This approach uses the OWASP HtmlSanitizer project.

  • User Input - Input validation is extensively applied. All data received from users is vetted against white lists whenever possible. Data types and character sets are validated and controlled. Open redirects are not allowed and Broadleaf does not employ patterns that would cause redirect directives to be emitted using user generated content.

  • Policies - We employ policy management at the resource tier that enforces access control based on OAuth claims, in addition to more elaborate suitability requirements based on context and user.

Securing Resource Microservices

Broadleaf’s OAuth support for resource-tier microservices is fairly easy to consume. We provide auto-configured libraries (broadleaf-oauth2-resource-security and broadleaf-oauth2-resource-security) that automatically set up basic configuration for spring security and oauth, as long as the library jars are on the classpath. Both libraries are required when using oauth. This approach is used for all out-of-the-box Broadleaf microservices.

Customization

ResourceSecurityDsl

com.broadleafcommerce.oauth2.resource.security.configurers.ResourceSecurityDsl is a custom DSL used to initialize the Spring Security configuration. This DSL should be utilized explicitly within a SecurityFilterChain using resourceSecurity(), for example:

@Configuration
@EnableWebSecurity
public class MyWebSecurity {
  @Bean
  public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
      http.apply(ResourceSecurityDsl.resourceSecurity());
      http.authorizeHttpRequests(authorize -> authorize
                      .requestMatchers("/secured").authenticated()
                      .requestMatchers("/**").permitAll());
      return http.build();
  }
}

This DSL makes use of HttpSecurity.oauth2ResourceServer() under the covers to configure the resource security. This configuration can be modified to extend or override the out-of-box configuration provided by this DSL, for example:

@Configuration
@EnableWebSecurity
public class MyWebSecurity {
  @Bean
  public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
      http.apply(ResourceSecurityDsl.resourceSecurity());
      http.oauth2ResourceServer(oauth2 -> oauth2
                      .jwt(jwt -> jwt
                              .decoder(new MySpecialJwtDecoder())));
      return http.build();
  }
}

An alternative extension pattern one can use it to simply register a JwtDecoder bean. If registered, this bean will be used instead of the default JWT decoder, for example:

@Configuration
@EnableWebSecurity
public class MyWebSecurity {
  @Bean
  public SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
      http.apply(ResourceSecurityDsl.resourceSecurity());
      return http.build();
  }

  @Bean
  public JwtDecoder mySpecialJwtDecoder() {
      return new MySpecialJwtDecoder();
  }

  @Bean
  public OAuth2TokenValidator  mySpecialJwtValidator() {
      return new MySpecialJwtValidator();
  }

  @Bean
  public Converter  mySpecialAuthenticationConverter() {
      return new MySpecialJwtAuthenticationConverter();
  }
}

SecurityEnhancer

Fine-tuning of security for URI match patterns can also be achieved without necessarily replacing the out-of-the-box config. Bean instances of com.broadleafcommerce.resource.security.SecurityEnhancer will be automatically recognized and leveraged when using Broadleaf’s security library. For example, often the configuration extending from an adapter implementation like ResourceServerConfigurerAdapter (e.g. Broadleaf’s OAuth2SecurityConfig) includes a blanket request security requirement like:

http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/**").authenticated());

However, an individual resource tier service may want to allow a certain endpoint call to utilize anonymous authentication. A bean class implementing SecurityEnhancer could achieve this goal by providing configuration like so:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/content/**").permitAll());
}

The SecurityEnhancer’s configuration will be put into place before the adapter’s configuration, and will therefore take precedence. SecurityEnhancer instances are also ordered, so a SecurityEnhancer provided by Broadleaf can be overridden by providing a custom, competing SecurityEnhancer with a higher order precedence.

Note
It is possible to use an alternate security approach (e.g. not oauth) for microservices. Broadleaf services internally do not rely on oauth concepts specifically - rather, they rely on standard Spring Security concepts that are harvested from oauth. A different Spring Security approach could be chosen to similarly inform the microservice regarding authorization. However, an in-depth discussion of such an endeavor is beyond the scope of this document.

Customizing XSS Protection

Configuration of PolicyFactory instance for OWASP HtmlSanitizer is achieved via a convenient fluent API. Refer to com.broadleafcommerce.resource.security.xss.service.XSSRequestProcessingService to review the default configuration of this component. Customization of this configuration is achieved by simply creating a custom bean definition using the xssRequestProcessingService bean name and emitting a XSSRequestProcessingService instance using the desired PolicyFactory configuration.

Policy

There are generally two phases to security upon request entry into a Broadleaf microservice. The first phase involves the Spring Security layer described above. The second phase involves a Broadleaf specific policy management architecture that further qualifies a request based on additional Broadleaf-specific factors:

  • Allow access to resources or functions to only users with correct:

    • Access to the requested context (ContextInfo)

    • Permissions for the requested resource (eg. Product)

    • Permissions for the requested operation (eg. Read vs Update)

    • Ownership of the requested resource (eg. Customer’s own profile)

  • Prevent access to trackable resources from the wrong context

  • Prevent mutation of trackable resources from the wrong context

Policy determinations are implied via code markup using the @Policy annotation. It is at these junctures that Broadleaf will make policy determinations, and either allow a request to succeed, or fail the request with an exception. Policy annotations are most often seen at REST endpoint method signatures, and at Spring Data repository interface method signatures.

Note
The types of exceptions will differ somewhat based on the case. Information leakage is avoided where appropriate and exceptions are not always bubbled to the caller as security level HTTP statuses. For example, a lack of visibility determination by the policy architecture will result in a 404 (Not Found) status back to the caller.

Visibility

Qualification of the visibility of a request entity operation is performed by comparing the authorization provided in the Spring Security state (usually harvested from oauth bearer token) to the policy defined for the request flow. The types of validations performed include: tenant membership, application membership, catalog visibility, customer context (in the case or Cart, Order, and Customer domain, for example), and sandbox visibility. For more information on tenant, application, and catalog concepts, refer to Multi-Tenancy. For more information on sandbox related concepts, refer to Data-Workflow.

Permission

Permission validation is used to check if a user has permission to perform the requested operation on the resource.

Permission validation only occurs if the policy has defined permission roots. PRODUCT is an example of a permission root.

Operation type is required to determine what permission will be necessary. This is retrieved from the policy or the context info. CREATE, READ, UPDATE, DELETE are the usual operation types, with UNKNOWN being used if none could be determined.

To achieve a complete permission, the operation type is combined with the permission roots - for example, the operation “READ” with the root “PRODUCT” would require the permission “READ_PRODUCT”. If there is no way to determine the operation type, then the “UNKNOWN” operation type will produce an “ALL” permission.

The actual validation occurs when the required permissions are checked against the user’s authorities. The user may need to have any or all of these required permissions, based on the policy’s matching strategy. Usually the ANY strategy is used, but there are some cases where the user must have ALL the specified permissions. For example, a “promote and deploy” endpoint method will require the user to be able to both promote and deploy, which are different permissions in the system.

Some user permissions can also be implied by others. The user gets the “READ” operation version of the permission for free if they have any of the other operation permissions for that root. If the user has a permission for the “ALL” operation, then they get all the others for free for that root.

Here’s an example @Policy markup for a method that requires READ capabilities on a product:

@Policy(permissionRoots = "PRODUCT", operationTypes = OperationType.READ,
        permissionMatchingStrategy = PermissionMatchingStrategy.ANY)

Ownership

There is also ownership validation, which commonly occurs for domain types belonging to a specific user. This includes domain concepts such as Customer, Cart, Order, etc…​ This kind of validation occurs if the policy has the “OWNER” identity type. The user must have a claim with the same name defined by the policy’s owner identifier. So, for the customer example, the policy’s owner identifier would be “customer_id”. Customers have this claim, so they will have their ownership validated. However, admins do not have this claim, so they can access customer freely without any ownership check. Ownership validation can only be used for resource entities which are owned - this is an interface which allows accessing the entity’s owner identifier to see who owns it (see com.broadleafcommerce.data.tracking.core.policy.Owned).

During ownership validation, the user’s owner identifier is retrieved from the specified claim, and checked for match with up to 3 other values:

  • User’s owner ID needs to match the value of the order identifier parameter if it is defined by the policy. This ensures the user is requesting their own data for endpoints where the owner identifier is also a request parameter, like “read orders for customer”.

  • If there’s some entity being passed in to the method, like in the update example below, it can be identified using the “param” property of the policy to make sure it’s validated. For ownership, this means it must be confirmed the user is the owner of this owned entity being mutated.

  • Finally, after a read operation, it must be confirmed the user is actually the owner of the owned entity being returned. For example, if a customer is trying to read an order directly by ID, once the order has been read, it will be validated for correct ownership.

Figure: Update Policy Example

@Policy(permissionRoots = {"CUSTOMER_PROFILE"}
        identityTypes = {IdentityType.ADMIN, IdentityType.OWNER},
        ownerIdentifier = “customer_id”,
        ownerIdentifierParam = 0,
        param = 1)
@FrameworkGetMapping("/customers/{customerId}")
public void updateCustomer(
        @PathVariable("customerId") String customerId,
        @RequestBody Customer customer ...

Mutability

When mutation of an entity is requested, validation of its mutability from the current context must be determined. The entity to validate for mutability will be indicated by the policy’s param property. The validations to perform depend on the entity’s trackable behaviors.

  • Tenant trackable entity:

    • An entity in a tenant can only be changed in that tenant. So, the tenant of the entity must match the context’s tenant.

    • If the entity is only tenant trackable (meaning it doesn’t have any other trackable behaviors), then it’s only mutable from the tenant level context. There must be no requested application context.

  • Application trackable entity:

    • The only validation to perform for application trackable behavior is to ensure that the entity’s application matches the context’s application.

  • Catalog trackable entity:

    • Some catalogs are marked as “hidden”, so entities belonging to them are not mutable

    • If our current context is within an application where the entity’s catalog is directly assigned, then the entity is mutable only if the application catalog’s mutability type is customizable

    • Some catalogs exclude applications from adding new entities, so a check is required to determine if the current context application is permitted in order to insert new entities into that catalog

Refer to to Sandboxing In Detail for more information on domain design as it relates to @TrackableExtension and TrackableBehavior.

Complete Policy Example

@Policy(permissionRoots = {"CUSTOMER", "CUSTOMER_PROFILE"}                                      # (1)
        operationTypes = OperationType.UPDATE,                                                  # (2)
        permissionMatchingStrategy = PermissionMatchingStrategy.ANY,                            # (3)
        identityTypes = {IdentityType.ADMIN, IdentityType.OWNER},                               # (4)
        ownerIdentifier = “customer_id”,                                                        # (5)
        ownerIdentifierParam = 0,                                                               # (6)
        param = 1)                                                                              # (7)
@FrameworkGetMapping("/customers/{customerId}")
public void updateCustomer(
        @PathVariable("customerId") String customerId,                                          # (6)
        @RequestBody Customer customer,                                                         # (7)
        @ContextOperation(OperationType.UPDATE) ContextInfo contextInfo) {...}                  # (2)
  1. Permission roots are the part of the required permission which usually describes the resource type being accessed. In this case, both “CUSTOMER” (which an admin user can access), and “CUSTOMER_PROFILE” (which is more limited access available to the customer) are available.

  2. Operation types are the kinds of operations that can be performed here. This is often left blank, which defaults to “UNKNOWN”, and then the operation type is inferred from the ContextInfo. In this case, if the ContextInfo also doesn’t have an operation type, then the “ALL” type permission will be required. This could also have multiple values, in which case the ContextInfo is checked for matches to one of them. If not, then the first one in the list is used.

  3. The permission matching strategy indicates whether the user needs to have ALL or just ANY of the permissions pulled from combining the permission roots with the operation types. Since this is ANY, users with either “UPDATE_CUSTOMER” or “UPDATE_CUSTOMER_PROFILE” permission access are permitted.

  4. The identity types indicate what kind of users have access. Since this contains “OWNER”, extra checks for ownership will be performed.

  5. Owner identifier is the claim property which will serve to identify a possible resource owner. Only users which have this claim property will be validated for ownership.

  6. Owner identifier param indicates which of the parameters must match the user’s owner identifier. In this case, the method has a parameter indicating the ID of the customer to perform an update on.

  7. Param indicates a parameter which represents an entity that’s going to be mutating. This is only applicable to methods which take in an entity and perform some change, like inserts or updates. This entity will have mutability validations performed on it, and in this case, ownership validations as well.

Policy Merge

Policies can be declared in a purposefully anemic fashion - providing only the information that is known at a given point in the flow. With policy merge, multiple policy instances encountered in a flow are merged together into a more informative policy on which to base a decision. Using such an approach, an initial policy can declare general root level information, while a deeper policy can specify more granular policy information specific to that point in the flow. This is often employed with the combination of endpoint definition and repository definition policies. The former sets up basic information for the flow, and the repository establishes specific information about the operation (e.g. READ or UPDATE). This provides a sane way to re-use a repository concept (e.g TrackableRepository) with multiple domain types. Here is an example demonstrating such a configuration:

Figure: Customer Endpoint

@Policy(permissionRoots = {"CUSTOMER", "CUSTOMER_PROFILE"}
        identityTypes = {IdentityType.ADMIN, IdentityType.OWNER},
        ownerIdentifierParam = 0)
@FrameworkGetMapping("/customers/{customerId}")
public void updateCustomer(
        @PathVariable("customerId") String customerId,
        @RequestBody Customer customer,
        @ContextOperation(OperationType.UPDATE) ContextInfo contextInfo) {...}

Figure: TrackableRepository

@Policy(operationTypes = {OperationType.UPDATE, OperationType.DELETE,  OperationType.CREATE},
        param = 0)
D save(@NonNull D entity, @Nullable ContextInfo contextInfo);

Policy Override

It is possible to override the behavior of a Policy declared by Broadleaf and achieve customized behavior. A PolicyOverride bean can be declared (see com.broadleafcommerce.data.tracking.core.policy.PolicyOverride) to either change an existing policy, or remove it entirely. Furthermore, PolicyOverride can be used to introduce new policies without using the annotation. Interestingly, this can be used to introduce policies in even non-Broadleaf code (e.g. a policy covering a Spring bean). This latter case requires a new custom PolicyAspect to target correct class (see com.broadleafcommerce.data.tracking.core.policy.PolicyOverride for more information).

@Bean
PolicyOverride saveOverride() {
    return new PolicyOverride("save.*")
         .withPermissionRoots(new String[] {"OTHER"})
         .withOperationTypes(new OperationType[] {OperationType.UPDATE})
         .withAspect(DefaultPolicyAspect.class);
}

Other Topics

Refer to Architecture - Security for additional security related information.