Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5383f03
increase version for next increment
bechte Feb 24, 2026
69cdf6b
Merge final Release 5.0.0 into develop
bechte Feb 24, 2026
8493a0d
additional conditions and handlers for cxdevproxy
bechte Feb 27, 2026
1529002
finalize documentation, extract interface from JwtTokenService and pr…
bechte Feb 27, 2026
e60beab
redesign of proxy internals:
bechte Mar 2, 2026
f733181
update documentation
bechte Mar 2, 2026
6311e17
add tests for conditions
bechte Mar 2, 2026
b38b48c
add tests for proxy interceptor builder
bechte Mar 2, 2026
45527fc
add tests for JWT injector interceptor
bechte Mar 2, 2026
bd54087
introduce prefix convention for interceptor beans
bechte Mar 2, 2026
b7680d2
introduce interceptors DSL and add tests for interceptors
bechte Mar 2, 2026
6344c44
add tests for util package
bechte Mar 2, 2026
29313bc
add tests for SSL handling
bechte Mar 2, 2026
9ca7b03
add tests for message source
bechte Mar 2, 2026
88cf673
add tests for token service
bechte Mar 2, 2026
98c1d30
add tests for rule engine
bechte Mar 2, 2026
1a12171
add tests for startup page handler incl localizations in all languages
bechte Mar 2, 2026
000e386
add tests for static content handler
bechte Mar 2, 2026
47968d5
add tests for template rendering handler
bechte Mar 2, 2026
892e875
update documentation
bechte Mar 2, 2026
d429219
fix double classpath: notation due to use of ClasspathUtils
bechte Mar 4, 2026
55137ff
switch to HTTP by default, ssl needs activation
bechte Mar 4, 2026
7893a3c
use short notation in groovy rules and in documentation
bechte Mar 4, 2026
9480233
stop processing of request, once processing has started or is completed
bechte Mar 4, 2026
d18f9e1
fix broken links in documentation
bechte Mar 4, 2026
9961035
set release version of extensions to 5.0.1
bechte Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# CX DEV Workspace

[![Build & Test](https://github.com/cxdevtools/workspace/actions/workflows/buildandtest.yml/badge.svg)](https://github.com/cxdevtools/workspace/actions/workflows/buildandtest.yml)
[![Code Analysis](https://github.com/cxdevtools/workspace/actions/workflows/code-analysis.yml/badge.svg)](https://github.com/cxdevtools/workspace/actions/workflows/code-analysis.yml)
[![Code Coverage](https://codecov.io/gh/cxdevtools/workspace/branch/main/graph/badge.svg?token=F1BIK8R7NZ)](https://codecov.io/gh/cxdevtools/workspace)
[![Dependency Check](https://github.com/cxdevtools/workspace/actions/workflows/dependency-check.yml/badge.svg)](https://github.com/cxdevtools/workspace/actions/workflows/dependency-check.yml)
[![Build & Test](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/buildandtest.yml/badge.svg)](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/buildandtest.yml)
[![Code Analysis](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/code-analysis.yml/badge.svg)](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/code-analysis.yml)
[![Code Coverage](https://codecov.io/gh/cxdevtools/sap-commerce-cloud/branch/main/graph/badge.svg?token=F1BIK8R7NZ)](https://codecov.io/gh/cxdevtools/sap-commerce-cloud)
[![Dependency Check](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/dependency-check.yml/badge.svg)](https://github.com/cxdevtools/sap-commerce-cloud/actions/workflows/dependency-check.yml)

All extensions available in this repository are built with high test coverage and do not influence the behavior of
your project without changes to your configuration. This is guaranteed and intended by design, because the extensions
Expand All @@ -14,8 +14,8 @@ configuration parameter.
## Contributing

Contributions are both welcomed and appreciated. For specific guidelines regarding contributions, please see
[CONTRIBUTING.md](https://github.com/cxdevtools/workspace/blob/main/CONTRIBUTING.md) in the root directory of the
[CONTRIBUTING.md](https://github.com/cxdevtools/sap-commerce-cloud/blob/main/CONTRIBUTING.md) in the root directory of the
project. Those willing to use milestone or SNAPSHOT releases are encouraged to file feature requests and bug reports
using the project's [issue tracker](https://github.com/cxdevtools/workspace/issues). Issues marked with an
<a href="https://github.com/cxdevtools/workspace/labels/help%20wanted">`help wanted`</a> label are specifically
using the project's [issue tracker](https://github.com/cxdevtools/sap-commerce-cloud/issues). Issues marked with an
<a href="https://github.com/cxdevtools/sap-commerce-cloud/labels/help%20wanted">`help wanted`</a> label are specifically
targeted for community contributions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<extensioninfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="extensioninfo.xsd">
<extension abstractclassprefix="Generated" classprefix="CxBackoffice" jaloLogicFree="true" name="cxdevbackoffice" version="5.0.0" usemaven="true">
<extension abstractclassprefix="Generated" classprefix="CxBackoffice" jaloLogicFree="true" name="cxdevbackoffice" version="5.0.1" usemaven="true">
<requires-extension name="cxdevtoolkit"/>
<requires-extension name="backoffice"/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>me.cxdev</groupId>
<artifactId>cxdevbackoffice</artifactId>
<version>5.0.0</version>
<version>5.0.1</version>
<packaging>jar</packaging>
<dependencies>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<extensioninfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="extensioninfo.xsd">
<extension abstractclassprefix="Generated" classprefix="CxEnvConfig" jaloLogicFree="true"
name="cxdevenvconfig" version="5.0.0" usemaven="true">
name="cxdevenvconfig" version="5.0.1" usemaven="true">
<requires-extension name="commercewebservices"/>
<coremodule generated="true" manager="de.hybris.platform.jalo.extension.GenericManager" packageroot="me.cxdev.commerce.config"/>
</extension>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>me.cxdev</groupId>
<artifactId>cxdevenvconfig</artifactId>
<version>5.0.0</version>
<version>5.0.1</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
This repository has a special setup for contributing.

Please read the [CONTRIBUTING.md from the extensions repository](https://github.com/sapcxtools/workspace/blob/main/CONTRIBUTING.md) which
Please read the [CONTRIBUTING.md from the extensions repository](https://github.com/cxdevtools/sap-commerce-cloud/blob/main/CONTRIBUTING.md) which
will guide you through the process.
220 changes: 151 additions & 69 deletions core-customize/hybris/bin/custom/cxdevtools/cxdevproxy/README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,182 @@
# CX Dev Proxy

The `cxdevproxy` extension improves and simplifies the local development experience for SAP Commerce by providing an
embedded, highly configurable Undertow reverse proxy.
The **CX Dev Proxy** is a powerful, Undertow-based local development proxy extension for SAP Commerce. It acts as a transparent reverse proxy in front of your local SAP Commerce Tomcat instance, solving the most common frontend and headless development pain points out-of-the-box.

## FEATURE DESCRIPTION

This extension solves common local routing and authentication challenges when developing headless storefronts (like
Spartacus/Angular) against a local SAP Commerce backend. It provides a seamless "all-in-one" endpoint for developers.

In addition, it contributes several major features to the local development lifecycle.
## 🚀 Key Features

### Embedded Reverse Proxy & Header Forwarding
* **Dynamic Groovy DSL & Hot-Reloading:** Define routing rules and HTTP modifications using a highly readable, fluent Groovy DSL. Save the script and the proxy updates instantly with **zero downtime**—no server restarts required!
* **Clean Interceptor Architecture:** A strict separation between Request Routing (Conditions) and Request Modification (Interceptors) ensures predictable, thread-safe request manipulation.
* **Zero-Config JWT Mocking:** Automatically injects valid JWT tokens for local development. It uses the platform's native `jwkSource`, meaning tokens are fully trusted by SAP Commerce without any backend configuration.
* **Developer Auth Portal:** An out-of-the-box, bilingual UI portal (`/proxy/login.html`, supporting English, German, and more via `Accept-Language`) to easily switch between mock Employee, B2C, and B2B user contexts.
* **Auto-CORS:** Automatically handles Cross-Origin Resource Sharing (CORS) preflight requests, echoing the incoming Origin header. Perfect for local Angular/React/Vue apps running on different ports (e.g., `localhost:4200`).
* **Conflict-Free Configuration:** Securely injects Spring backend properties and i18n messages into frontend templates using custom `%` and `#` syntax, eliminating collisions with modern JavaScript.

A lightweight Undertow server that listens on a unified port (e.g., `8080`) and dynamically routes traffic to either the
local frontend dev server (e.g., Angular on `4200`) or the SAP Commerce backend (Tomcat on `9002`).
The `ForwardedHeadersHandler` automatically injects `X-Forwarded-Host`, `X-Forwarded-Proto`, and `X-Forwarded-Port`
headers. This prevents infinite HTTPS redirect loops from Spring Security and ensures that Tomcat generates absolute
URLs correctly.
---

### Developer Portal & JWT Mocking
## 🛠 Configuration & Properties

Provides a local Developer Portal (accessible via the root or `index.html`) to easily switch between different mocked
user sessions (Employees or Customers).
When a user is selected, the `JwtInjectorHandler` intercepts backend requests, dynamically generates a signed JWT using
the local domain's private key (via Nimbus JOSE+JWT), and injects it as a `Bearer` token. The static JWT claims can be
easily managed via JSON templates.
You can configure the core behavior via your `local.properties` (or `project.properties`).

### Startup Interception
```properties
# -----------------------------------------------------------------------
# CX Dev Proxy - Core Configuration
# -----------------------------------------------------------------------
# Enables or disables the proxy
cxdevproxy.enabled=true

The `StartupPageHandler` listens to the Hybris tenant lifecycle. While the master tenant is starting up or shutting
down, all incoming proxy requests are intercepted, and a localized, auto-refreshing "503 Service Unavailable"
maintenance page is served to prevent hanging requests or backend errors.
# Port on which the proxy will listen
cxdevproxy.server.port=8080

### Modular Static Content
# --- Dynamic Routing Rules (Groovy DSL) ---
# Paths to the Groovy scripts defining the proxy rules.
# Supports 'classpath:' (inside exploded extensions) and 'file:' (absolute path on disk).
cxdevproxy.proxy.frontend.rules=classpath:cxdevproxy/rulesets/cxdevproxy-frontend-rules.groovy
cxdevproxy.proxy.backend.rules=classpath:cxdevproxy/rulesets/cxdevproxy-backend-rules.groovy

The `StaticContentHandler` allows serving static files (HTML, CSS, JS, images) directly from the classpath without
invoking the backend server. Files placed in `resources/cxdevproxy/static-content/` are automatically served.
# --- UI & Auth Portal Configuration ---
# Toggle visibility of customer tabs in the /proxy/login.html portal
cxdevproxy.proxy.ui.login.showB2C=false
cxdevproxy.proxy.ui.login.showB2B=true

### Extensible Conditional Handlers
# --- JWT Mocking Configuration ---
# Specifies the base path where the proxy looks for JWT claim templates (JSON files).
cxdevproxy.proxy.jwt.templatepath=classpath:cxdevproxy/jwt

The proxy pipeline is highly customizable. The `ConditionalDelegateHandler` allows executing specific handlers only if a
set of conditions is met (e.g., matching HTTP methods, specific paths, or headers). It supports complex logical
expressions via `AndCondition`, `OrCondition`, and `NotCondition`.
# Defines the validity duration of the generated mock JWT tokens.
# Supported formats: 500ms, 60s, 10m, 10h, 1d (Defaults to ms if no unit is provided)
cxdevproxy.proxy.jwt.validity=10h
```

## How to activate and use
---

To activate this feature, simply set the `cxdevproxy.enabled` property to `true` in your `local.properties`.
## 🖥 Developer Auth Portal & Safe Properties

**Adding Static Content:**
Other extensions can contribute to the proxy's static files (like adding new pages to the Developer Portal) by simply
placing files inside their own `resources/cxdevproxy/static-content/` directory. The proxy classloader will pick them up
automatically.
The extension provides a built-in UI to set mock user cookies. You can access it via `/proxy/login.html`.

**Adding new Mock Users (JWT Templates):**
To add a new mock user, create a JSON file with the static claims in `resources/cxdevproxy/jwt/employee/<id>.json` or
`resources/cxdevproxy/jwt/customer/<id>.json`.
To prevent syntax collisions between Spring property resolution and modern JavaScript template literals (`${...}`), the HTML templates utilize a custom, robust placeholder syntax:
* **`%{property.key:defaultValue}`** for Spring Configuration Properties
* **`#{i18n.message.key:Default Text}`** for localized Message Bundles

**Adding Custom Handlers and Conditions:**
You can extend the proxy routing by defining new conditions and handlers in your Spring configuration. Simply create
your custom conditions and inject them into a `ConditionalDelegateHandler` via Spring XML:
This allows you to safely toggle UI elements based on your backend configuration without breaking frontend scripts:

```xml
<bean id="myCustomPathCondition" class="me.cxdev.commerce.proxy.condition.PathStartsWithCondition">
<property name="prefix" value="/my-custom-api/" />
</bean>
```javascript
// Safely resolved by the proxy's TemplateRenderingHandler before reaching the browser
const showB2C = %{cxdevproxy.proxy.ui.login.showB2C:false};
const showB2B = %{cxdevproxy.proxy.ui.login.showB2B:false};
```

Then add your handler to the `backendHandlers` list of the `UndertowProxyManager` bean.
---

## 🧩 Building Routing Rules (The Groovy DSL)

Instead of verbose XML, the CX Dev Proxy uses a powerful Groovy Domain Specific Language (DSL). The scripts are hot-reloaded the moment you save them.



To provide the best Developer Experience, our rule engine **automatically imports** all fluent condition factories (`Conditions.*`) and interceptor builders (`Interceptors.*`), and dynamically binds existing Spring beans to the script context.

### 1. Fluent API (Conditions & Interceptors)

You can build complex routes using our functional, AssertJ-style API directly in the script:

**Conditions:**
* `isMethod("GET")`, `pathStartsWith("/occ")`, `pathMatches("/occ/v2/**")`, `pathRegexMatches(".*")`
* `hasHeader("Authorization")`, `hasCookie("cxdevproxy_user_id")`, `hasParameter("username")`
* **Logical Operators:** `and(...)`, `or(...)`, `not(...)`, `always()`, `never()`

**Inline Interceptors:**
* `jsonResponse(200, '{"status":"ok"}')`
* `htmlResponse("<h1>Hello</h1>")`
* `networkDelay("800ms")` or `networkDelay("1s", "3s")`

### 2. Pre-configured Spring Variables & Magic Naming

The script environment is automatically populated with context-aware variables (derived from your Spring XML) to make routing effortless.

**Custom Bean Naming Convention:**
If you write your own Spring beans for Conditions or Interceptors, follow this prefix convention. The proxy engine will automatically strip the prefix and bind the bean using lower-camel-case:
* Bean `cxdevproxyConditionIsOcc` becomes `isOcc` in Groovy.
* Bean `cxdevproxyInterceptorCxJwtInjector` becomes `cxJwtInjector` in Groovy.

**Available Pre-bound Variables:**
* **Conditions:** `isOcc`, `isSmartEdit`, `isBackoffice`, `isAdminConsole`, `isAuthorizationServer`, `hasMockUser`, `hasAuthorizationHeader`
* **Interceptors:** `forwardedHeaders`, `jwtInjector`, `corsInjector`

---

## 💡 Usage Examples

To add custom rules for your project, first override the default rule paths in your `local.properties` to point to your custom project directory:

## Configuration parameters
```properties
cxdevproxy.proxy.backend.rules=classpath:path/to/your/project/my-backend-rules.groovy
cxdevproxy.proxy.frontend.rules=classpath:path/to/your/project/my-frontend-rules.groovy
```

Every script must return a `List<ProxyExchangeInterceptor>`. Rules are evaluated top-to-bottom.

### 1. The Baseline (Default Script)

The most basic setup simply applies standard proxy headers unconditionally:

```groovy
return [
forwardedHeaders
]
```

### 2. Conditional Execution (The Builder Pattern)

You should never mutate the state of injected Spring beans directly. Instead, wrap them using the stateless `interceptor()` builder to apply them conditionally.

```groovy
// 1. Combine pre-configured conditions seamlessly using static logical operators
def jwtCondition = and(or(isOcc, isSmartEdit), hasMockUser, not(hasAuthorizationHeader))

// 2. Simple API Mocking with inline JSON
def mockCartCall = interceptor()
.constrainedBy( isMethod("GET"), pathMatches("/**/carts/current") )
.perform( jsonResponse('{"type": "cartWsDTO", "totalItems": 5}') )

// 3. Simulating Latency
def slowCheckout = interceptor()
.constrainedBy( isMethod("POST"), pathMatches("/**/orders") )
.perform( networkDelay("1s", "3s") )

return [
forwardedHeaders, // Always execute

// Execute JWT Injector only if the complex condition is met
interceptor()
.constrainedBy(jwtCondition)
.perform(jwtInjector),

// Execute CORS Injector only for OCC requests
interceptor()
.constrainedBy(isOcc)
.perform(corsInjector),

mockCartCall,
slowCheckout
]
```

| Parameter | Type | Description |
|-----------|------|-------------|
| cxdevproxy.enabled | boolean | Feature toggle. Must be set to `true` to start the Undertow proxy server (default: false). |
| cxdevproxy.application-context | String | Specifies the location of the spring context file automatically added to the global platform application context. |
| cxdevproxy.ssl.enabled | boolean | Enables HTTPS for the Undertow server. If false, forces HTTP routing. |
| cxdevproxy.ssl.keystore.path | String | Absolute path to the PKCS12 keystore used for SSL offloading and JWT signing. |
| cxdevproxy.ssl.keystore.password | String | Password for the configured keystore. |
| cxdevproxy.ssl.keystore.alias | String | Alias of the private key within the keystore used for SSL and JWT signing. |
| cxdevproxy.server.bindaddress | String | Network interface the proxy listens on (e.g., `127.0.0.1` or `0.0.0.0`). |
| cxdevproxy.server.protocol | String | The protocol exposed by the proxy (`http` or `https`). |
| cxdevproxy.server.hostname | String | The public hostname of the proxy (e.g., `local.cxdev.me`). Used for the `X-Forwarded-Host` header. |
| cxdevproxy.server.port | int | The port the Undertow proxy listens on (default: `8080`). |
| cxdevproxy.proxy.frontend.protocol | String | Protocol for frontend routing (e.g., `http` or `https`). |
| cxdevproxy.proxy.frontend.hostname | String | Hostname of the local frontend dev server (e.g., `localhost`). |
| cxdevproxy.proxy.frontend.port | int | Port of the local frontend dev server (e.g., `4200`). |
| cxdevproxy.proxy.backend.protocol | String | Protocol for backend routing (e.g., `https`). |
| cxdevproxy.proxy.backend.hostname | String | Hostname of the local SAP Commerce backend (e.g., `localhost`). |
| cxdevproxy.proxy.backend.port | int | Port of the local SAP Commerce backend (e.g., `9002`). |
| cxdevproxy.proxy.backend.contexts | String | Comma-separated list of paths to route to the backend. If empty, uses auto-discovery via webroot properties. |
---

## 🔑 JWT Mocking Deep Dive

## License
The `jwtInjector` is the heart of the local authentication bypass. It intercepts requests (when conditions match) and injects a dynamically signed JWT.

_Licensed under the Apache License, Version 2.0, January 2004_
> **⚠️ IMPORTANT: OAuth Client ID Requirement**
> By default, our provided B2C and B2B user templates use `storefront` as the `client_id`. This breaks with the SAP Commerce default (which uses `mobile_android` for OCC).
> To make the mock tokens work, you must ensure an OAuth Client with the ID `storefront` is created in your local SAP Commerce database (via ImpEx).
> We understand that this might be questionable, but we are convinced that using a self-explaining `client_id` is a best-practise and should be enforced anyway. If you don't agree, feel free to change the templates to your needs.

_Copyright 2026, SAP CX Tools_
1. **Native Trust:** It extracts the private key from the platform's `jwkSource` (the exact same one used by the `authorizationserver`). This means the backend trusts the generated tokens implicitly. No extra backend configuration needed!
2. **Templates:** It loads static claims from JSON files. For example, if the cookies are `user_type=customer` and `user_id=john@example.com`, it looks for a template at:
`classpath:cxdevproxy/jwt/customer/john@example.com.json`
3. **Dynamic Claims:** Claims like `iat` (Issued At) and `exp` (Expiration) are dynamically calculated based on `cxdevproxy.proxy.jwt.validity` and automatically cache-managed to prevent mid-flight expirations.

To customize templates per project, simply set `cxdevproxy.proxy.jwt.templatepath=path/to/your/custom/templates` in your local properties.
Loading
Loading